refactor: Convert resolution to two-tier system

Typed-function's sort order/matching algorithm was interfering with
  template resolution. This commit solves the difficulty by moving the
  "catchall" implementations that implement generation of new template
  instances into a separate "fallback" typed-function universe, so that
  Pocomath can control exactly when that is searched.

  Removes a couple of the matching anomalies already noted in the tests.

  Also extends return types to somewhat more functions.
This commit is contained in:
Glen Whitney 2022-08-29 09:30:07 -04:00
parent a2f76a55b8
commit de42c22ab4
13 changed files with 428 additions and 278 deletions

View File

@ -2,15 +2,13 @@ import {Returns, returnTypeOf} from '../../core/Returns.mjs'
import PocomathInstance from '../../core/PocomathInstance.mjs'
const Complex = new PocomathInstance('Complex')
// Base type that should generally not be used directly
Complex.installType('Complex', {
test: z => z && typeof z === 'object' && 're' in z && 'im' in z
})
// Now the template type: Complex numbers are actually always homogeneous
// in their component types.
// in their component types. For an explanation of the meanings of the
// properties, see ../../tuple/Types/Tuple.mjs
Complex.installType('Complex<T>', {
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),
base: z => z && typeof z === 'object' && 're' in z && 'im' in z,
test: testT => z => testT(z.re) && testT(z.im),
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),
from: {
T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T)
U: convert => u => {

View File

@ -3,6 +3,7 @@ export * from './Types/Complex.mjs'
export const abs = {
'Complex<T>': ({
T,
sqrt, // Unfortunately no notation yet for the needed signature
'absquare(T)': baseabsq,
'absquare(Complex<T>)': absq

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
/* Returns true if w is z multiplied by a complex unit */
@ -9,9 +10,9 @@ export const associate = {
'one(T)': uno,
'complex(T,T)': cplx,
'negate(Complex<T>)': neg
}) => (w,z) => {
}) => Returns('boolean', (w,z) => {
if (eq(w,z) || eq(w,neg(z))) return true
const ti = times(z, cplx(zr(z.re), uno(z.im)))
return eq(w,ti) || eq(w,neg(ti))
}
})
}

View File

@ -6,13 +6,15 @@ export const multiply = {
T,
'complex(T,T)': cplx,
'add(T,T)': plus,
'subtract(T,T)': sub,
'subtract(T,T)': subt,
'self(T,T)': me,
'conjugate(T)': conj // makes quaternion multiplication work
}) => Returns(
`Complex<${T}>`,
(w,z) => cplx(
sub(me(w.re, z.re), me(conj(w.im), z.im)),
plus(me(conj(w.re), z.im), me(w.im, z.re)))
(w,z) => {
const realpart = subt(me( w.re, z.re), me(conj(w.im), z.im))
const imagpart = plus(me(conj(w.re), z.im), me( w.im, z.re))
return cplx(realpart, imagpart)
}
)
}

View File

@ -9,6 +9,7 @@ const anySpec = {} // fixed dummy specification of 'any' type
const theTemplateParam = 'T' // First pass: only allow this one exact parameter
const restTemplateParam = `...${theTemplateParam}`
const templateCall = `<${theTemplateParam}>`
const templateFromParam = 'U' // For defining covariant conversions
/* Returns a new signature just like sig but with the parameter replaced by
@ -21,6 +22,8 @@ function substituteInSignature(signature, parameter, type) {
return sig.replaceAll(pattern, type)
}
let lastWhatToDo = null // used in an infinite descent check
export default class PocomathInstance {
/* Disallowed names for ops; beware, this is slightly non-DRY
* in that if a new top-level PocomathInstance method is added, its name
@ -55,24 +58,39 @@ export default class PocomathInstance {
this._affects = {}
this._typed = typed.create()
this._typed.clear()
this._typed.addTypes([{name: 'ground', test: () => true}])
// The following is an additional typed-function universe for resolving
// uninstantiated template instances. It is linked to the main one in
// its onMismatch function, below:
this._metaTyped = typed.create()
this._metaTyped.clear()
// And these are the meta bindings: (I think we don't need separate
// invalidation for them as they are only accessed through a main call.)
this._meta = {} // The resulting typed-functions
this._metaTFimps = {} // and their implementations
const me = this
const myTyped = this._typed
this._typed.onMismatch = (name, args, sigs) => {
if (me._invalid.has(name)) {
return me[name](...args) // rebuild implementation and try again
// rebuild implementation and try again
return me[name](...args)
}
myTyped.throwMismatchError(name, args, sigs)
const metaversion = me._meta[name]
if (metaversion) {
return me._meta[name](...args)
}
me._typed.throwMismatchError(name, args, sigs)
}
/* List of types installed in the instance. We start with just dummies
* for the 'any' type and for type parameters:
*/
// List of types installed in the instance: (We start with just dummies
// for the 'any' type and for type parameters.)
this.Types = {any: anySpec}
this.Types[theTemplateParam] = anySpec
this.Types.ground = anySpec
// All the template types that have been defined
// Types that have been moved into the metaverse:
this._metafiedTypes = new Set()
// All the template types that have been defined:
this.Templates = {}
// The actual type testing functions
// And their instantiations:
this._instantiationsOf = {}
// The actual type testing functions:
this._typeTests = {}
// For each type, gives all of its (in)direct subtypes in topo order:
this._subtypes = {}
@ -87,27 +105,19 @@ export default class PocomathInstance {
this._maxDepthSeen = 1 // deepest template nesting we've actually encountered
this._invalid = new Set() // methods that are currently invalid
this._config = {predictable: false, epsilon: 1e-12}
const self = this
this.config = new Proxy(this._config, {
get: (target, property) => target[property],
set: (target, property, value) => {
if (value !== target[property]) {
target[property] = value
self._invalidateDependents('config')
me._invalidateDependents('config')
}
return true // successful
}
})
this._plainFunctions = new Set() // the names of the plain functions
this._chainRepository = {} // place to store chainified functions
this._installFunctions({
typeOf: {
ground: {uses: new Set(), does: () => Returns('string', () => 'any')}
}
})
this.joinTypes = this.joinTypes.bind(this)
this.joinTypes = this.joinTypes.bind(me)
}
/**
@ -330,9 +340,10 @@ export default class PocomathInstance {
`Type name '${type}' reserved for template parameter`)
}
if (parts.some(this._templateParam.bind(this))) {
// It's a template, deal with it separately
// It's an uninstantiated template, deal with it separately
return this._installTemplateType(type, spec)
}
const base = parts[0]
if (type in this.Types) {
if (spec !== this.Types[type]) {
throw new SyntaxError(`Conflicting definitions of type ${type}`)
@ -345,7 +356,7 @@ export default class PocomathInstance {
}
let beforeType = spec.refines
if (!beforeType) {
beforeType = 'ground'
beforeType = 'any'
for (const other of spec.before || []) {
if (other in this.Types) {
beforeType = other
@ -387,6 +398,21 @@ export default class PocomathInstance {
for (const subtype of this._subtypes[from]) {
this._priorTypes[nextSuper].add(subtype)
}
/* Add the conversion in the metaverse if need be: */
const toParts = nextSuper.split('<', 2)
if (toParts.length > 1) {
const fromParts = from.split('<', 2)
if (fromParts.length === 1 || fromParts[0] !== toParts[0]) {
this._metafy(from)
try {
this._metaTyped.addConversion(
{from, to: toParts[0], convert: spec.from[from]})
} catch {
}
}
}
nextSuper = this.Types[nextSuper].refines
}
}
@ -411,6 +437,16 @@ export default class PocomathInstance {
convert: this.Types[to].from[fromtype]
})
this._invalidateDependents(':' + nextSuper)
/* Add the conversion in the metaverse if need be: */
const toParts = nextSuper.split('<', 2)
if (toParts.length > 1 && base !== toParts[0]) {
this._metafy(type)
this._metaTyped.addConversion({
from: type,
to: toParts[0],
convert: this.Types[to].from[fromtype]
})
}
} catch {
}
this._priorTypes[nextSuper].add(type)
@ -425,6 +461,12 @@ export default class PocomathInstance {
this._installFunctions({typeOf: imp})
})
_metafy(type) {
if (this._metafiedTypes.has(type)) return
this._metaTyped.addTypes([{name: type, test: this._typeTests[type]}])
this._metafiedTypes.add(type)
}
_addSubtypeTo(sup, sub) {
if (this.isSubtypeOf(sub, sup)) return
const supSubs = this._subtypes[sup]
@ -435,8 +477,10 @@ export default class PocomathInstance {
supSubs.splice(i, 0, sub)
}
/* Returns true if typeA is a subtype of type B */
/* Returns true if typeA is a strict subtype of type B */
isSubtypeOf = Returns('boolean', function(typeA, typeB) {
// Currently not considering types to be a subtype of 'any'
if (typeB === 'any' || typeA === 'any') return false
return this._subtypes[typeB].includes(typeA)
})
@ -483,7 +527,6 @@ export default class PocomathInstance {
if (!typeA) return typeB
if (!typeB) return typeA
if (typeA === 'any' || typeB === 'any') return 'any'
if (typeA === 'ground' || typeB === 'ground') return 'ground'
if (typeA === typeB) return typeA
const subber = convert ? this._priorTypes : this._subtypes
const pick = convert ? 'has' : 'includes'
@ -510,7 +553,8 @@ export default class PocomathInstance {
* signatures of operations, but which have not actually been installed:
*/
undefinedTypes = Returns('Array<string>', function() {
return Array.from(this._seenTypes).filter(t => !(t in this.Types))
return Array.from(this._seenTypes).filter(
t => !(t in this.Types || t in this.Templates))
})
/* Used internally to install a template type */
@ -526,6 +570,18 @@ export default class PocomathInstance {
}
return
}
// install the "base type" in the meta universe:
let beforeType = 'any'
for (const other of spec.before || []) {
if (other in this.templates) {
beforeType = other
break
}
}
this._metaTyped.addTypes([{name: base, test: spec.base}], beforeType)
this._instantiationsOf[base] = new Set()
// update the typeOf function
const imp = {}
imp[type] = {
@ -534,6 +590,9 @@ export default class PocomathInstance {
}
this._installFunctions({typeOf: imp})
// Invalidate any functions that reference this template type:
this._invalidateDependents(':' + base)
// Nothing else actually happens until we match a template parameter
this.Templates[base] = {type, spec}
}
@ -578,12 +637,11 @@ export default class PocomathInstance {
}
opImps[signature] = {
explicit,
resolved: false,
uses: behavior.uses,
does: behavior.does
}
if (explicit) {
opImps[signature].resolved = false
} else {
if (!explicit) {
opImps[signature].hasInstantiations = {}
opImps[signature].needsInstantiations = new Set()
}
@ -628,6 +686,8 @@ export default class PocomathInstance {
_invalidate(name, reason) {
if (!(name in this._imps)) {
this._imps[name] = {}
this._TFimps[name] = {}
this._metaTFimps[name] = {}
}
if (reason) {
// Make sure no TF imps that depend on reason remain:
@ -692,10 +752,8 @@ export default class PocomathInstance {
if (!imps) {
throw new SyntaxError(`No implementations for ${name}`)
}
if (!(this._TFimps[name])) {
this._TFimps[name] = {}
}
const tf_imps = this._TFimps[name]
const meta_imps = this._metaTFimps[name]
/* Collect the entries we know the types for */
const usableEntries = []
for (const entry of Object.entries(imps)) {
@ -751,6 +809,79 @@ export default class PocomathInstance {
}
}
/* Prevent other existing signatures from blocking use of top-level
* templates via conversions:
*/
let baseSignature = rawSignature.replaceAll(templateCall, '')
/* Any remaining template params are top-level */
const signature = substituteInSignature(
baseSignature, theTemplateParam, 'any')
const hasTopLevel = (signature !== baseSignature)
if (!ubType && hasTopLevel) {
// collect upper-bound types
const ubTypes = new Set()
for (const othersig in imps) {
const thisUB = upperBounds.exec(othersig)
if (thisUB) ubTypes.add(thisUB[2])
let basesig = othersig.replaceAll(templateCall, '')
if (basesig !== othersig) {
// A template
const testsig = substituteInSignature(
basesig, theTemplateParam, '')
if (testsig === basesig) {
// that is not also top-level
for (const templateType of typeListOfSignature(basesig)) {
if (templateType.slice(0,3) === '...') {
templateType = templateType.slice(3)
}
ubTypes.add(templateType)
}
}
}
}
for (const othersig in imps) {
let basesig = othersig.replaceAll(templateCall, '')
const testsig = substituteInSignature(
basesig, theTemplateParam, '')
if (testsig !== basesig) continue // a top-level template
for (let othertype of typeListOfSignature(othersig)) {
if (othertype.slice(0,3) === '...') {
othertype = othertype.slice(3)
}
if (this.Types[othertype] === anySpec) continue
const testType = substituteInSignature(
othertype, theTemplateParam, '')
let otherTypeCollection = [othertype]
if (testType !== othertype) {
const base = othertype.split('<',1)[0]
otherTypeCollection = this._instantiationsOf[base]
}
for (const possibility of otherTypeCollection) {
for (const convtype of this._priorTypes[possibility]) {
if (this.isSubtypeOf(convtype, possibility)) continue
if (ubTypes.has(convtype)) continue
let belowUB = false
for (const anUB of ubTypes) {
if (anUB in this.Templates) {
if (convtype.slice(0, anUB.length) === anUB) {
belowUB = true
break
}
} else {
if (this.isSubtypeOf(convtype, anUB)) {
belowUB = true
break
}
}
}
if (belowUB) continue
instantiationSet.add(convtype)
}
}
}
}
}
for (const instType of instantiationSet) {
if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue
@ -785,8 +916,7 @@ export default class PocomathInstance {
}
return behavior.does(innerRefs)
}
this._addTFimplementation(
tf_imps, signature, {uses, does: patch})
this._addTFimplementation(tf_imps, signature, {uses, does: patch})
tf_imps[signature]._pocoSignature = rawSignature
tf_imps[signature]._pocoInstance = instType
behavior.hasInstantiations[instType] = signature
@ -794,13 +924,7 @@ export default class PocomathInstance {
/* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */
if (ubType) continue
if ('_catchall_' in behavior.hasInstantiations) continue
let templateCall = `<${theTemplateParam}>`
/* Relying here that the base of 'Foo<T>' is 'Foo': */
let baseSignature = rawSignature.replaceAll(templateCall, '')
/* Any remaining template params are top-level */
const signature = substituteInSignature(
baseSignature, theTemplateParam, 'ground')
if (behavior.resolved) continue
/* The catchall signature has to detect the actual type of the call
* and add the new instantiations.
* First, prepare the type inference data:
@ -840,150 +964,124 @@ export default class PocomathInstance {
/* For return type annotation, we may have to fix this to
propagate the return type. At the moment we are just bagging
*/
const patch = (refs) => (...args) => {
/* We unbundle the rest arg if there is one */
const regLength = args.length - 1
if (restParam) {
const restArgs = args.pop()
args = args.concat(restArgs)
}
/* Now infer the type we actually should have been called for */
let i = -1
let j = -1
/* collect the arg types */
const argTypes = []
for (const arg of args) {
++j
// in case of rest parameter, reuse last parameter type:
if (i < inferences.length - 1) ++i
if (inferences[i]) {
const argType = inferences[i](arg)
if (!argType) {
throw TypeError(
`Type inference failed for argument ${j} of ${name}`)
}
if (argType === 'any') {
throw TypeError(
`In call to ${name}, incompatible template arguments: `
// + args.map(a => JSON.stringify(a)).join(', ')
// unfortunately barfs on bigints. Need a better formatter
// wish we could just use the one that console.log uses;
// is that accessible somehow?
+ args.map(a => a.toString()).join(', ')
+ ' of types ' + argTypes.join(', ') + argType)
}
argTypes.push(argType)
const patch = () => {
const patchFunc = (...tfBundledArgs) => {
/* We unbundle the rest arg if there is one */
let args = Array.from(tfBundledArgs)
const regLength = args.length - 1
if (restParam) {
const restArgs = args.pop()
args = args.concat(restArgs)
}
}
if (argTypes.length === 0) {
throw TypeError('Type inference failed for' + name)
}
let usedConversions = false
let instantiateFor = self.joinTypes(argTypes)
if (instantiateFor === 'any') {
usedConversions = true
instantiateFor = self.joinTypes(argTypes, usedConversions)
/* Now infer the type we actually should have been called for */
let i = -1
let j = -1
/* collect the arg types */
const argTypes = []
for (const arg of args) {
++j
// in case of rest parameter, reuse last parameter type:
if (i < inferences.length - 1) ++i
if (inferences[i]) {
const argType = inferences[i](arg)
if (!argType) {
throw TypeError(
`Type inference failed for argument ${j} of ${name}`)
}
if (argType === 'any') {
throw TypeError(
`In call to ${name}, `
+ 'incompatible template arguments:'
// + args.map(a => JSON.stringify(a)).join(', ')
// unfortunately barfs on bigints. Need a better
// formatter. I wish we could just use the one that
// console.log uses; is that accessible somehow?
+ args.map(a => a.toString()).join(', ')
+ ' of types ' + argTypes.join(', ') + argType)
}
argTypes.push(argType)
}
}
if (argTypes.length === 0) {
throw TypeError('Type inference failed for' + name)
}
let usedConversions = false
let instantiateFor = self.joinTypes(argTypes)
if (instantiateFor === 'any') {
throw TypeError(
`In call to ${name}, no type unifies arguments `
+ args.toString() + '; of types ' + argTypes.toString()
+ '; note each consecutive pair must unify to a '
+ 'supertype of at least one of them')
}
}
const depth = instantiateFor.split('<').length
if (depth > self._maxDepthSeen) {
self._maxDepthSeen = depth
}
/* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor))
const wantSig = wantTypes.join(',')
/* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal parameter
* type in the invocation, with the parameter template instantiated
* by instantiateFor, and for all of instantiateFor's "prior types"
*/
for (j = 0; j < parTypes.length; ++j) {
if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) {
// actually used the param and is a template
self._ensureTemplateTypes(parTypes[j], instantiateFor)
}
}
/* Transform the arguments if we used any conversions: */
if (usedConversions) {
i = - 1
for (j = 0; j < args.length; ++j) {
if (i < parTypes.length - 1) ++i
let wantType = parTypes[i]
if (wantType.slice(0,3) === '...') {
wantType = wantType.slice(3)
}
wantType = substituteInSignature(
wantType, theTemplateParam, instantiateFor)
if (wantType !== parTypes[i]) {
args[j] = self._typed.convert(args[j], wantType)
usedConversions = true
instantiateFor = self.joinTypes(argTypes, usedConversions)
if (instantiateFor === 'any') {
throw TypeError(
`In call to ${name}, no type unifies arguments `
+ args.toString() + '; of types ' + argTypes.toString()
+ '; note each consecutive pair must unify to a '
+ 'supertype of at least one of them')
}
}
}
/* Finally reassemble the rest args if there were any */
if (restParam) {
const restArgs = args.slice(regLength)
args = args.slice(0,regLength)
args.push(restArgs)
}
/* Arrange that the desired instantiation will be there next
* time so we don't have to go through that again for this type
*/
behavior.needsInstantiations.add(instantiateFor)
self._invalidate(name)
// And update refs because we now know the type we're instantiating
// for:
refs[theTemplateParam] = instantiateFor
const innerRefs = {}
for (const dep in simplifiedUses) {
const simplifiedDep = simplifiedUses[dep]
if (dep === simplifiedDep) {
innerRefs[dep] = refs[dep]
} else {
let [func, needsig] = dep.split(/[()]/)
if (self._typed.isTypedFunction(refs[simplifiedDep])) {
const subsig = substituteInSignature(
needsig, theTemplateParam, instantiateFor)
let resname = simplifiedDep
if (resname == 'self') resname = name
innerRefs[dep] = self.resolve(
resname, subsig, refs[simplifiedDep])
} else {
innerRefs[dep] = refs[simplifiedDep]
const depth = instantiateFor.split('<').length
if (depth > self._maxDepthSeen) {
self._maxDepthSeen = depth
}
/* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor))
const wantSig = wantTypes.join(',')
/* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal
* parameter type in the invocation, with the parameter
* template instantiated by instantiateFor, and for all of
* instantiateFor's "prior types"
*/
for (j = 0; j < parTypes.length; ++j) {
if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) {
// actually used the param and is a template
self._ensureTemplateTypes(parTypes[j], instantiateFor)
}
}
}
// Finally ready to make the call.
const implementation = behavior.does(innerRefs)
// We can access return type information here
// And in particular, if it might be a template, we should try to
// instantiate it:
const returnType = returnTypeOf(implementation, wantSig, self)
for (const possibility of returnType.split('|')) {
const instantiated = self._maybeInstantiate(possibility)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
self._invalidateDependents(':' + tempBase)
/* Request the desired instantiation: */
// But possibly since this resolution was grabbed, the proper
// instantiation has been added (like if there are multiple
// uses in the implementation of another method.
if (!(behavior.needsInstantiations.has(instantiateFor))) {
behavior.needsInstantiations.add(instantiateFor)
self._invalidate(name)
}
const brandNewMe = self[name]
const whatToDo = self._typed.resolve(brandNewMe, args)
// We can access return type information here
// And in particular, if it might be a template, we should try to
// instantiate it:
const returnType = returnTypeOf(whatToDo.fn, wantSig, self)
for (const possibility of returnType.split('|')) {
const instantiated = self._maybeInstantiate(possibility)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
self._invalidateDependents(':' + tempBase)
}
}
if (whatToDo === lastWhatToDo) {
throw new Error(
`Infinite recursion in resolving $name called on`
+ args.map(x => x.toString()).join(','))
}
lastWhatToDo = whatToDo
const retval = whatToDo.implementation(...args)
lastWhatToDo = null
return retval
}
return implementation(...args)
Object.defineProperty(
patchFunc, 'name', {value: `${name}(${signature})`})
return patchFunc
}
Object.defineProperty(patch, 'name', {value: `${name}(${signature})`})
// TODO: Decorate patch with a function that calculates the
Object.defineProperty(
patch, 'name', {value: `generate[${name}(${signature})]`})
// TODO?: Decorate patch with a function that calculates the
// correct return type a priori. Deferring because unclear what
// aspects will be merged into typed-function.
//
// The actual uses value needs to be a set:
const outerUses = new Set(Object.values(simplifiedUses))
this._addTFimplementation(
tf_imps, signature, {uses: outerUses, does: patch})
behavior.hasInstantiations._catchall_ = rawSignature
meta_imps, signature, {uses: new Set(), does: patch})
behavior.resolved = true
}
this._correctPartialSelfRefs(name, tf_imps)
// Make sure we have all of the needed (template) types; and if they
@ -1007,8 +1105,19 @@ export default class PocomathInstance {
delete fromBehavior.hasInstantiations[imp._pocoInstance]
}
}
const tf = this._typed(name, tf_imps)
Object.defineProperty(tf, 'fromInstance', {value: this})
let tf
if (Object.keys(tf_imps).length > 0) {
tf = this._typed(name, tf_imps)
Object.defineProperty(tf, 'fromInstance', {value: this})
}
let metaTF
if (Object.keys(meta_imps).length > 0) {
metaTF = this._metaTyped(name, meta_imps)
Object.defineProperty(metaTF, 'fromInstance', {value: this})
}
this._meta[name] = metaTF
tf = tf || metaTF
Object.defineProperty(this, name, {configurable: true, value: tf})
return tf
}
@ -1077,7 +1186,10 @@ export default class PocomathInstance {
'typed-function does not support mixed full and '
+ 'partial self-reference')
}
if (subsetOfKeys(typesOfSignature(needsig), this.Types)) {
const needTypes = typesOfSignature(needsig)
const mergedTypes = Object.assign(
{}, this.Types, this.Templates)
if (subsetOfKeys(needTypes, mergedTypes)) {
part_self_references.push(needsig)
}
}
@ -1097,10 +1209,19 @@ export default class PocomathInstance {
* accumulating:
*/
if (needsig) {
const tempTF = this._typed('dummy_' + func, this._TFimps[func])
let typedUniverse
let tempTF
if (Object.keys(this._TFimps[func]).length > 0) {
typedUniverse = this._typed
tempTF = typedUniverse('dummy_' + func, this._TFimps[func])
} else {
typedUniverse = this._metaTyped
tempTF = typedUniverse(
'dummy_' + func, this._metaTFimps[func])
}
let result = undefined
try {
result = this._typed.find(tempTF, needsig, {exact: true})
result = typedUniverse.find(tempTF, needsig, {exact: true})
} catch {}
if (result) {
refs[dep] = result
@ -1158,7 +1279,7 @@ export default class PocomathInstance {
return
}
const implementation = does(refs)
// could do something with return type information here
// could do something with return type information here?
imps[signature] = implementation
}
@ -1168,38 +1289,53 @@ export default class PocomathInstance {
const deferral = imps[aSignature]
const part_self_references = deferral.psr
const corrected_self_references = []
const remaining_self_references = []
const refs = deferral.builtRefs
for (const neededSig of part_self_references) {
// Have to find a match for neededSig among the other signatures
// of this function. That's a job for typed-function, but we will
// try here:
if (neededSig in imps) { // the easy case
corrected_self_references.push(neededSig)
remaining_self_references.push(neededSig)
continue
}
// No exact match, try to get one that matches with
// subtypes since the whole conversion thing in typed-function
// is too complicated to reproduce
const foundSig = this._findSubtypeImpl(name, imps, neededSig)
let foundSig = this._findSubtypeImpl(name, imps, neededSig)
if (foundSig) {
corrected_self_references.push(foundSig)
remaining_self_references.push(neededSig)
} else {
throw new Error(
'Implement inexact self-reference in typed-function for '
+ `${name}(${neededSig})`)
// Maybe it's a template instance we don't yet have
foundSig = this._findSubtypeImpl(
name, this._imps[name], neededSig)
if (foundSig) {
const match = this._pocoFindSignature(name, neededSig)
refs[`self(${neededSig})`] = match.implementation
} else {
throw new Error(
'Implement inexact self-reference in typed-function for '
+ `${name}(${neededSig})`)
}
}
}
const refs = deferral.builtRefs
const does = deferral.sigDoes
imps[aSignature] = this._typed.referTo(
...corrected_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i]
if (remaining_self_references.length > 0) {
imps[aSignature] = this._typed.referTo(
...corrected_self_references, (...impls) => {
for (let i = 0; i < remaining_self_references.length; ++i) {
refs[`self(${remaining_self_references[i]})`] = impls[i]
}
const implementation = does(refs)
// What will we do with the return type info in here?
return implementation
}
const implementation = does(refs)
// What will we do with the return type info in here?
return implementation
}
)
)
} else {
imps[aSignature] = does(refs)
}
imps[aSignature]._pocoSignature = deferral._pocoSignature
imps[aSignature]._pocoInstance = deferral._pocoInstance
}
@ -1247,7 +1383,7 @@ export default class PocomathInstance {
if (wantsType in this.Types) return false
// OK, need to generate the type from the template
// Set up refines, before, test, and from
const newTypeSpec = {refines: base}
const newTypeSpec = {}
const maybeFrom = {}
const template = this.Templates[base].spec
if (!template) {
@ -1255,6 +1391,11 @@ export default class PocomathInstance {
`Implementor error in instantiateTemplate(${base}, ${instantiator})`)
}
const instantiatorSpec = this.Types[instantiator]
if (instantiatorSpec.refines) {
this.instantiateTemplate(base, instantiatorSpec.refines)
// Assuming our templates are covariant, I guess
newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>`
}
let beforeTypes = []
if (instantiatorSpec.before) {
beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`)
@ -1268,18 +1409,15 @@ export default class PocomathInstance {
if (beforeTypes.length > 0) {
newTypeSpec.before = beforeTypes
}
newTypeSpec.test = template.test(this._typeTests[instantiator])
const templateTest = template.test(this._typeTests[instantiator])
newTypeSpec.test = x => (template.base(x) && templateTest(x))
if (template.from) {
for (let source in template.from) {
const instSource = substituteInSignature(
source, theTemplateParam, instantiator)
let usesFromParam = false
for (const word of instSource.split(/[<>]/)) {
if (word === templateFromParam) {
usesFromParam = true
break
}
}
const testSource = substituteInSignature(
instSource, templateFromParam, instantiator)
const usesFromParam = (testSource !== instSource)
if (usesFromParam) {
for (const iFrom in instantiatorSpec.from) {
const finalSource = substituteInSignature(
@ -1287,11 +1425,13 @@ export default class PocomathInstance {
maybeFrom[finalSource] = template.from[source](
instantiatorSpec.from[iFrom])
}
// Assuming all templates are covariant here, I guess...
for (const subType of this._subtypes[instantiator]) {
const finalSource = substituteInSignature(
instSource, templateFromParam, subType)
maybeFrom[finalSource] = template.from[source](x => x)
if (testSource !== wantsType) { // subtypes handled with refines
// Assuming all templates are covariant here, I guess...
for (const subType of this._subtypes[instantiator]) {
const finalSource = substituteInSignature(
instSource, templateFromParam, subType)
maybeFrom[finalSource] = template.from[source](x => x)
}
}
} else {
maybeFrom[instSource] = template.from[source]
@ -1303,6 +1443,7 @@ export default class PocomathInstance {
newTypeSpec.from = maybeFrom
}
this.installType(wantsType, newTypeSpec)
this._instantiationsOf[base].add(wantsType)
return wantsType
})
@ -1314,7 +1455,7 @@ export default class PocomathInstance {
const otherTypeList = typeListOfSignature(otherSig)
if (typeList.length !== otherTypeList.length) continue
let allMatch = true
let paramBound = 'ground'
let paramBound = 'any'
for (let k = 0; k < typeList.length; ++k) {
let myType = typeList[k]
let otherType = otherTypeList[k]
@ -1326,8 +1467,7 @@ export default class PocomathInstance {
otherTypeList[k] = `...${paramBound}`
otherType = paramBound
}
const adjustedOtherType = otherType.replaceAll(
`<${theTemplateParam}>`, '')
const adjustedOtherType = otherType.replaceAll(templateCall, '')
if (adjustedOtherType !== otherType) {
otherTypeList[k] = adjustedOtherType
otherType = adjustedOtherType
@ -1342,22 +1482,21 @@ export default class PocomathInstance {
theTemplateParam, paramBound)
}
if (otherType === 'any') continue
if (otherType === 'ground') continue
if (!(otherType in this.Types)) {
allMatch = false
break
}
if (myType === otherType
|| this.isSubtypeOf(myType, otherType)) {
continue
}
if (myType === otherType) continue
if (otherType in this.Templates) {
const myBase = myType.split('<',1)[0]
if (myBase === otherType) continue
if (this.instantiateTemplate(otherType, myType)) {
let dummy
dummy = this[name] // for side effects
return this._findSubtypeImpl(name, this._imps[name], neededSig)
}
}
if (!(otherType in this.Types)) {
allMatch = false
break
}
if (this.isSubtypeOf(myType, otherType)) continue
allMatch = false
break
}
@ -1373,19 +1512,42 @@ export default class PocomathInstance {
if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name]
}
let result = undefined
const haveTF = this._typed.isTypedFunction(typedFunction)
if (haveTF) {
// First try a direct match
let result
try {
result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch {
}
if (result) return result
// Next, look ourselves but with subtypes:
const wantTypes = typeListOfSignature(sig)
for (const [implSig, details]
of typedFunction._typedFunctionData.signatureMap) {
let allMatched = true
const implTypes = typeListOfSignature(implSig)
for (let i = 0; i < wantTypes.length; ++i) {
if (wantTypes[i] == implTypes[i]
|| this.isSubtypeOf(wantTypes[i], implTypes[i])) continue
allMatched = false
break
}
if (allMatched) return details
}
}
if (result || !(this._imps[name])) return result
if (!(this._imps[name])) return undefined
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) {
if (haveTF) {
return this._typed.findSignature(typedFunction, foundsig)
try {
return this._typed.findSignature(typedFunction, foundsig)
} catch {
}
}
try {
return this._metaTyped.findSignature(this._meta[name], foundsig)
} catch {
}
// We have an implementation but not a typed function. Do the best
// we can:
@ -1398,20 +1560,8 @@ export default class PocomathInstance {
const pseudoImpl = foundImpl.does(needs)
return {fn: pseudoImpl, implementation: pseudoImpl}
}
const wantTypes = typeListOfSignature(sig)
for (const [implSig, details]
of typedFunction._typedFunctionData.signatureMap) {
let allMatched = true
const implTypes = typeListOfSignature(implSig)
for (let i = 0; i < wantTypes.length; ++i) {
if (wantTypes[i] == implTypes[i]
|| this.isSubtypeOf(wantTypes[i], implTypes[i])) continue
allMatched = false
break
}
if (allMatched) return details
}
// Hmm, no luck. Make sure bundle is up-to-date and retry:
let result = undefined
typedFunction = this[name]
try {
result = this._typed.findSignature(typedFunction, sig)

View File

@ -1,7 +1,9 @@
import Returns from '../core/Returns.mjs'
export const abs = {
T: ({
T,
'smaller(T,T)': lt,
'negate(T)': neg,
'zero(T)': zr
}) => t => (smaller(t, zr(t)) ? neg(t) : t)
}) => Returns(T, t => (smaller(t, zr(t)) ? neg(t) : t))
}

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs'
export const abs = {number: () => n => Math.abs(n)}
export const abs = {'T:number': ({T}) => Returns(T, n => Math.abs(n))}

View File

@ -2,24 +2,22 @@
import PocomathInstance from '../../core/PocomathInstance.mjs'
const Tuple = new PocomathInstance('Tuple')
// First a base type that will generally not be used directly
Tuple.installType('Tuple', {
test: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts)
})
// Now the template type that is the primary use of this
Tuple.installType('Tuple<T>', {
// We are assuming that any 'Type<T>' refines 'Type', so this is
// not necessary:
// refines: 'Tuple',
// But we need there to be a way to determine the type of a tuple:
infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)),
// For the test, we can assume that t is already a base tuple,
// and we get the test for T as an input and we have to return
// the test for Tuple<T>
// A test that "defines" the "base type", which is not really a type
// (only fully instantiated types are added to the universe)
base: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts),
// The template portion of the test; it takes the test for T as
// input and returns the test for an entity _that already passes
// the base test_ to be a Tuple<T>:
test: testT => t => t.elts.every(testT),
// These are only invoked for types U such that there is already
// a conversion from U to T, and that conversion is passed as an input
// and we have to return the conversion to Tuple<T>:
// And we need there to be a way to determine the (instantiation)
// type of an tuple (that has already passed the base test):
infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)),
// Conversions. Parametrized conversions are only invoked for types
// U such that there is already a conversion from U to T, and that
// conversion is passed as an input, and we have to return the conversion
// function from the indicated template in terms of U to Tuple<T>:
from: {
'Tuple<U>': convert => tu => ({elts: tu.elts.map(convert)}),
// Here since there is no U it's a straight conversion:

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export {Tuple} from './Types/Tuple.mjs'
export const length = {Tuple: () => t => t.elts.length}
export const length = {'Tuple<T>': () => Returns('NumInt', t => t.elts.length)}

View File

@ -46,6 +46,9 @@ describe('The default full pocomath instance "math"', () => {
assert.strictEqual(math.returnTypeOf('identity', 'Fraction'), 'Fraction')
assert.strictEqual(
math.returnTypeOf('quotient', 'bigint,bigint'), 'bigint')
math.abs(math.complex(2,1)) //TODO: ditto
assert.strictEqual(
math.returnTypeOf('abs','Complex<NumInt>'), 'number')
})
it('can subtract numbers', () => {

View File

@ -39,8 +39,8 @@ describe('complex', () => {
})
it('checks for equality', () => {
assert.ok(math.equal(math.complex(3,0), 3))
assert.ok(math.equal(math.complex(3,2), math.complex(3, 2)))
assert.ok(math.equal(math.complex(3, 0), 3))
assert.ok(math.equal(math.complex(3, 2), math.complex(3, 2)))
assert.ok(!(math.equal(math.complex(45n, 3n), math.complex(45n, -3n))))
assert.ok(!(math.equal(math.complex(45n, 3n), 45n)))
})

View File

@ -135,13 +135,8 @@ describe('A custom instance', () => {
assert.strictEqual(
inst.typeMerge(3, inst.complex(4.5,2.1)),
'Merge to Complex<number>')
// The following is the current behavior, since 3 converts to 3+0i
// which is technically the same Complex type as 3n+0ni.
// This should clear up when Complex is templatized
assert.strictEqual(inst.typeMerge(3, inst.complex(3n)), 'Merge to Complex')
// But types that truly cannot be merged should throw a TypeError
// Should add a variation of this with a more usual type once there is
// one not interconvertible with others...
assert.throws(
() => inst.typeMerge(3, inst.complex(3n)), TypeError)
inst.install(genericSubtract)
assert.throws(() => inst.typeMerge(3, undefined), TypeError)
})

View File

@ -8,14 +8,12 @@ describe('tuple', () => {
it('does not allow unification by converting consecutive arguments', () => {
assert.throws(() => math.tuple(3, 5.2, 2n), /TypeError.*unif/)
// Hence, the order matters in a slightly unfortunate way,
// but I think being a little ragged in these edge cases is OK:
assert.throws(
() => math.tuple(3, 2n, math.complex(5.2)),
/TypeError.*unif/)
assert.deepStrictEqual(
math.tuple(3, math.complex(2n), 5.2),
{elts: [math.complex(3), math.complex(2n), math.complex(5.2)]})
assert.throws(
() => math.tuple(3, math.complex(2n), 5.2),
/TypeError.*unif/)
})
it('can be tested for zero and equality', () => {