feat: Return type annotations #53

Merged
glen merged 15 commits from return_types into main 2022-08-30 19:36:44 +00:00
13 changed files with 428 additions and 278 deletions
Showing only changes of commit de42c22ab4 - Show all commits

View File

@ -2,15 +2,13 @@ import {Returns, returnTypeOf} from '../../core/Returns.mjs'
import PocomathInstance from '../../core/PocomathInstance.mjs' import PocomathInstance from '../../core/PocomathInstance.mjs'
const Complex = new PocomathInstance('Complex') 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 // 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>', { 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), test: testT => z => testT(z.re) && testT(z.im),
infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]),
from: { from: {
T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T) T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T)
U: convert => u => { U: convert => u => {

View File

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

View File

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

View File

@ -6,13 +6,15 @@ export const multiply = {
T, T,
'complex(T,T)': cplx, 'complex(T,T)': cplx,
'add(T,T)': plus, 'add(T,T)': plus,
'subtract(T,T)': sub, 'subtract(T,T)': subt,
'self(T,T)': me, 'self(T,T)': me,
'conjugate(T)': conj // makes quaternion multiplication work 'conjugate(T)': conj // makes quaternion multiplication work
}) => Returns( }) => Returns(
`Complex<${T}>`, `Complex<${T}>`,
(w,z) => cplx( (w,z) => {
sub(me(w.re, z.re), me(conj(w.im), z.im)), const realpart = subt(me( w.re, z.re), me(conj(w.im), z.im))
plus(me(conj(w.re), z.im), me(w.im, z.re))) 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 theTemplateParam = 'T' // First pass: only allow this one exact parameter
const restTemplateParam = `...${theTemplateParam}` const restTemplateParam = `...${theTemplateParam}`
const templateCall = `<${theTemplateParam}>`
const templateFromParam = 'U' // For defining covariant conversions const templateFromParam = 'U' // For defining covariant conversions
/* Returns a new signature just like sig but with the parameter replaced by /* 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) return sig.replaceAll(pattern, type)
} }
let lastWhatToDo = null // used in an infinite descent check
export default class PocomathInstance { export default class PocomathInstance {
/* Disallowed names for ops; beware, this is slightly non-DRY /* Disallowed names for ops; beware, this is slightly non-DRY
* in that if a new top-level PocomathInstance method is added, its name * in that if a new top-level PocomathInstance method is added, its name
@ -55,24 +58,39 @@ export default class PocomathInstance {
this._affects = {} this._affects = {}
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() 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 me = this
const myTyped = this._typed const myTyped = this._typed
this._typed.onMismatch = (name, args, sigs) => { this._typed.onMismatch = (name, args, sigs) => {
if (me._invalid.has(name)) { 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)
} }
/* List of types installed in the instance. We start with just dummies me._typed.throwMismatchError(name, args, sigs)
* 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 = {any: anySpec}
this.Types[theTemplateParam] = anySpec this.Types[theTemplateParam] = anySpec
this.Types.ground = anySpec // Types that have been moved into the metaverse:
// All the template types that have been defined this._metafiedTypes = new Set()
// All the template types that have been defined:
this.Templates = {} this.Templates = {}
// The actual type testing functions // And their instantiations:
this._instantiationsOf = {}
// The actual type testing functions:
this._typeTests = {} this._typeTests = {}
// For each type, gives all of its (in)direct subtypes in topo order: // For each type, gives all of its (in)direct subtypes in topo order:
this._subtypes = {} this._subtypes = {}
@ -87,27 +105,19 @@ export default class PocomathInstance {
this._maxDepthSeen = 1 // deepest template nesting we've actually encountered this._maxDepthSeen = 1 // deepest template nesting we've actually encountered
this._invalid = new Set() // methods that are currently invalid this._invalid = new Set() // methods that are currently invalid
this._config = {predictable: false, epsilon: 1e-12} this._config = {predictable: false, epsilon: 1e-12}
const self = this
this.config = new Proxy(this._config, { this.config = new Proxy(this._config, {
get: (target, property) => target[property], get: (target, property) => target[property],
set: (target, property, value) => { set: (target, property, value) => {
if (value !== target[property]) { if (value !== target[property]) {
target[property] = value target[property] = value
self._invalidateDependents('config') me._invalidateDependents('config')
} }
return true // successful return true // successful
} }
}) })
this._plainFunctions = new Set() // the names of the plain functions this._plainFunctions = new Set() // the names of the plain functions
this._chainRepository = {} // place to store chainified functions this._chainRepository = {} // place to store chainified functions
this.joinTypes = this.joinTypes.bind(me)
this._installFunctions({
typeOf: {
ground: {uses: new Set(), does: () => Returns('string', () => 'any')}
}
})
this.joinTypes = this.joinTypes.bind(this)
} }
/** /**
@ -330,9 +340,10 @@ export default class PocomathInstance {
`Type name '${type}' reserved for template parameter`) `Type name '${type}' reserved for template parameter`)
} }
if (parts.some(this._templateParam.bind(this))) { 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) return this._installTemplateType(type, spec)
} }
const base = parts[0]
if (type in this.Types) { if (type in this.Types) {
if (spec !== this.Types[type]) { if (spec !== this.Types[type]) {
throw new SyntaxError(`Conflicting definitions of type ${type}`) throw new SyntaxError(`Conflicting definitions of type ${type}`)
@ -345,7 +356,7 @@ export default class PocomathInstance {
} }
let beforeType = spec.refines let beforeType = spec.refines
if (!beforeType) { if (!beforeType) {
beforeType = 'ground' beforeType = 'any'
for (const other of spec.before || []) { for (const other of spec.before || []) {
if (other in this.Types) { if (other in this.Types) {
beforeType = other beforeType = other
@ -387,6 +398,21 @@ export default class PocomathInstance {
for (const subtype of this._subtypes[from]) { for (const subtype of this._subtypes[from]) {
this._priorTypes[nextSuper].add(subtype) 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 nextSuper = this.Types[nextSuper].refines
} }
} }
@ -411,6 +437,16 @@ export default class PocomathInstance {
convert: this.Types[to].from[fromtype] convert: this.Types[to].from[fromtype]
}) })
this._invalidateDependents(':' + nextSuper) 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 { } catch {
} }
this._priorTypes[nextSuper].add(type) this._priorTypes[nextSuper].add(type)
@ -425,6 +461,12 @@ export default class PocomathInstance {
this._installFunctions({typeOf: imp}) 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) { _addSubtypeTo(sup, sub) {
if (this.isSubtypeOf(sub, sup)) return if (this.isSubtypeOf(sub, sup)) return
const supSubs = this._subtypes[sup] const supSubs = this._subtypes[sup]
@ -435,8 +477,10 @@ export default class PocomathInstance {
supSubs.splice(i, 0, sub) 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) { 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) return this._subtypes[typeB].includes(typeA)
}) })
@ -483,7 +527,6 @@ export default class PocomathInstance {
if (!typeA) return typeB if (!typeA) return typeB
if (!typeB) return typeA if (!typeB) return typeA
if (typeA === 'any' || typeB === 'any') return 'any' if (typeA === 'any' || typeB === 'any') return 'any'
if (typeA === 'ground' || typeB === 'ground') return 'ground'
if (typeA === typeB) return typeA if (typeA === typeB) return typeA
const subber = convert ? this._priorTypes : this._subtypes const subber = convert ? this._priorTypes : this._subtypes
const pick = convert ? 'has' : 'includes' const pick = convert ? 'has' : 'includes'
@ -510,7 +553,8 @@ export default class PocomathInstance {
* signatures of operations, but which have not actually been installed: * signatures of operations, but which have not actually been installed:
*/ */
undefinedTypes = Returns('Array<string>', function() { 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 */ /* Used internally to install a template type */
@ -526,6 +570,18 @@ export default class PocomathInstance {
} }
return 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 // update the typeOf function
const imp = {} const imp = {}
imp[type] = { imp[type] = {
@ -534,6 +590,9 @@ export default class PocomathInstance {
} }
this._installFunctions({typeOf: imp}) 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 // Nothing else actually happens until we match a template parameter
this.Templates[base] = {type, spec} this.Templates[base] = {type, spec}
} }
@ -578,12 +637,11 @@ export default class PocomathInstance {
} }
opImps[signature] = { opImps[signature] = {
explicit, explicit,
resolved: false,
uses: behavior.uses, uses: behavior.uses,
does: behavior.does does: behavior.does
} }
if (explicit) { if (!explicit) {
opImps[signature].resolved = false
} else {
opImps[signature].hasInstantiations = {} opImps[signature].hasInstantiations = {}
opImps[signature].needsInstantiations = new Set() opImps[signature].needsInstantiations = new Set()
} }
@ -628,6 +686,8 @@ export default class PocomathInstance {
_invalidate(name, reason) { _invalidate(name, reason) {
if (!(name in this._imps)) { if (!(name in this._imps)) {
this._imps[name] = {} this._imps[name] = {}
this._TFimps[name] = {}
this._metaTFimps[name] = {}
} }
if (reason) { if (reason) {
// Make sure no TF imps that depend on reason remain: // Make sure no TF imps that depend on reason remain:
@ -692,10 +752,8 @@ export default class PocomathInstance {
if (!imps) { if (!imps) {
throw new SyntaxError(`No implementations for ${name}`) throw new SyntaxError(`No implementations for ${name}`)
} }
if (!(this._TFimps[name])) {
this._TFimps[name] = {}
}
const tf_imps = this._TFimps[name] const tf_imps = this._TFimps[name]
const meta_imps = this._metaTFimps[name]
/* Collect the entries we know the types for */ /* Collect the entries we know the types for */
const usableEntries = [] const usableEntries = []
for (const entry of Object.entries(imps)) { 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) { for (const instType of instantiationSet) {
if (!(instType in this.Types)) continue if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue if (this.Types[instType] === anySpec) continue
@ -785,8 +916,7 @@ export default class PocomathInstance {
} }
return behavior.does(innerRefs) return behavior.does(innerRefs)
} }
this._addTFimplementation( this._addTFimplementation(tf_imps, signature, {uses, does: patch})
tf_imps, signature, {uses, does: patch})
tf_imps[signature]._pocoSignature = rawSignature tf_imps[signature]._pocoSignature = rawSignature
tf_imps[signature]._pocoInstance = instType tf_imps[signature]._pocoInstance = instType
behavior.hasInstantiations[instType] = signature behavior.hasInstantiations[instType] = signature
@ -794,13 +924,7 @@ export default class PocomathInstance {
/* Now add the catchall signature */ /* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */ /* (Not needed if if it's a bounded template) */
if (ubType) continue if (ubType) continue
if ('_catchall_' in behavior.hasInstantiations) continue if (behavior.resolved) 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')
/* The catchall signature has to detect the actual type of the call /* The catchall signature has to detect the actual type of the call
* and add the new instantiations. * and add the new instantiations.
* First, prepare the type inference data: * First, prepare the type inference data:
@ -840,8 +964,10 @@ export default class PocomathInstance {
/* For return type annotation, we may have to fix this to /* For return type annotation, we may have to fix this to
propagate the return type. At the moment we are just bagging propagate the return type. At the moment we are just bagging
*/ */
const patch = (refs) => (...args) => { const patch = () => {
const patchFunc = (...tfBundledArgs) => {
/* We unbundle the rest arg if there is one */ /* We unbundle the rest arg if there is one */
let args = Array.from(tfBundledArgs)
const regLength = args.length - 1 const regLength = args.length - 1
if (restParam) { if (restParam) {
const restArgs = args.pop() const restArgs = args.pop()
@ -864,11 +990,12 @@ export default class PocomathInstance {
} }
if (argType === 'any') { if (argType === 'any') {
throw TypeError( throw TypeError(
`In call to ${name}, incompatible template arguments: ` `In call to ${name}, `
+ 'incompatible template arguments:'
// + args.map(a => JSON.stringify(a)).join(', ') // + args.map(a => JSON.stringify(a)).join(', ')
// unfortunately barfs on bigints. Need a better formatter // unfortunately barfs on bigints. Need a better
// wish we could just use the one that console.log uses; // formatter. I wish we could just use the one that
// is that accessible somehow? // console.log uses; is that accessible somehow?
+ args.map(a => a.toString()).join(', ') + args.map(a => a.toString()).join(', ')
+ ' of types ' + argTypes.join(', ') + argType) + ' of types ' + argTypes.join(', ') + argType)
} }
@ -900,9 +1027,10 @@ export default class PocomathInstance {
type, theTemplateParam, instantiateFor)) type, theTemplateParam, instantiateFor))
const wantSig = wantTypes.join(',') const wantSig = wantTypes.join(',')
/* Now we have to add any actual types that are relevant /* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal parameter * to this invocation. Namely, that would be every formal
* type in the invocation, with the parameter template instantiated * parameter type in the invocation, with the parameter
* by instantiateFor, and for all of instantiateFor's "prior types" * template instantiated by instantiateFor, and for all of
* instantiateFor's "prior types"
*/ */
for (j = 0; j < parTypes.length; ++j) { for (j = 0; j < parTypes.length; ++j) {
if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) {
@ -910,61 +1038,21 @@ export default class PocomathInstance {
self._ensureTemplateTypes(parTypes[j], instantiateFor) self._ensureTemplateTypes(parTypes[j], instantiateFor)
} }
} }
/* Transform the arguments if we used any conversions: */
if (usedConversions) { /* Request the desired instantiation: */
i = - 1 // But possibly since this resolution was grabbed, the proper
for (j = 0; j < args.length; ++j) { // instantiation has been added (like if there are multiple
if (i < parTypes.length - 1) ++i // uses in the implementation of another method.
let wantType = parTypes[i] if (!(behavior.needsInstantiations.has(instantiateFor))) {
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)
}
}
}
/* 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) behavior.needsInstantiations.add(instantiateFor)
self._invalidate(name) 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 brandNewMe = self[name]
} const whatToDo = self._typed.resolve(brandNewMe, args)
// Finally ready to make the call.
const implementation = behavior.does(innerRefs)
// We can access return type information here // We can access return type information here
// And in particular, if it might be a template, we should try to // And in particular, if it might be a template, we should try to
// instantiate it: // instantiate it:
const returnType = returnTypeOf(implementation, wantSig, self) const returnType = returnTypeOf(whatToDo.fn, wantSig, self)
for (const possibility of returnType.split('|')) { for (const possibility of returnType.split('|')) {
const instantiated = self._maybeInstantiate(possibility) const instantiated = self._maybeInstantiate(possibility)
if (instantiated) { if (instantiated) {
@ -972,18 +1060,28 @@ export default class PocomathInstance {
self._invalidateDependents(':' + tempBase) self._invalidateDependents(':' + tempBase)
} }
} }
return implementation(...args) if (whatToDo === lastWhatToDo) {
throw new Error(
`Infinite recursion in resolving $name called on`
+ args.map(x => x.toString()).join(','))
} }
Object.defineProperty(patch, 'name', {value: `${name}(${signature})`}) lastWhatToDo = whatToDo
// TODO: Decorate patch with a function that calculates the const retval = whatToDo.implementation(...args)
lastWhatToDo = null
return retval
}
Object.defineProperty(
patchFunc, 'name', {value: `${name}(${signature})`})
return patchFunc
}
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 // correct return type a priori. Deferring because unclear what
// aspects will be merged into typed-function. // 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( this._addTFimplementation(
tf_imps, signature, {uses: outerUses, does: patch}) meta_imps, signature, {uses: new Set(), does: patch})
behavior.hasInstantiations._catchall_ = rawSignature behavior.resolved = true
} }
this._correctPartialSelfRefs(name, tf_imps) this._correctPartialSelfRefs(name, tf_imps)
// Make sure we have all of the needed (template) types; and if they // 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] delete fromBehavior.hasInstantiations[imp._pocoInstance]
} }
} }
const tf = this._typed(name, tf_imps) let tf
if (Object.keys(tf_imps).length > 0) {
tf = this._typed(name, tf_imps)
Object.defineProperty(tf, 'fromInstance', {value: this}) 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}) Object.defineProperty(this, name, {configurable: true, value: tf})
return tf return tf
} }
@ -1077,7 +1186,10 @@ export default class PocomathInstance {
'typed-function does not support mixed full and ' 'typed-function does not support mixed full and '
+ 'partial self-reference') + '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) part_self_references.push(needsig)
} }
} }
@ -1097,10 +1209,19 @@ export default class PocomathInstance {
* accumulating: * accumulating:
*/ */
if (needsig) { 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 let result = undefined
try { try {
result = this._typed.find(tempTF, needsig, {exact: true}) result = typedUniverse.find(tempTF, needsig, {exact: true})
} catch {} } catch {}
if (result) { if (result) {
refs[dep] = result refs[dep] = result
@ -1158,7 +1279,7 @@ export default class PocomathInstance {
return return
} }
const implementation = does(refs) const implementation = does(refs)
// could do something with return type information here // could do something with return type information here?
imps[signature] = implementation imps[signature] = implementation
} }
@ -1168,38 +1289,53 @@ export default class PocomathInstance {
const deferral = imps[aSignature] const deferral = imps[aSignature]
const part_self_references = deferral.psr const part_self_references = deferral.psr
const corrected_self_references = [] const corrected_self_references = []
const remaining_self_references = []
const refs = deferral.builtRefs
for (const neededSig of part_self_references) { for (const neededSig of part_self_references) {
// Have to find a match for neededSig among the other signatures // Have to find a match for neededSig among the other signatures
// of this function. That's a job for typed-function, but we will // of this function. That's a job for typed-function, but we will
// try here: // try here:
if (neededSig in imps) { // the easy case if (neededSig in imps) { // the easy case
corrected_self_references.push(neededSig) corrected_self_references.push(neededSig)
remaining_self_references.push(neededSig)
continue continue
} }
// No exact match, try to get one that matches with // No exact match, try to get one that matches with
// subtypes since the whole conversion thing in typed-function // subtypes since the whole conversion thing in typed-function
// is too complicated to reproduce // is too complicated to reproduce
const foundSig = this._findSubtypeImpl(name, imps, neededSig) let foundSig = this._findSubtypeImpl(name, imps, neededSig)
if (foundSig) { if (foundSig) {
corrected_self_references.push(foundSig) corrected_self_references.push(foundSig)
remaining_self_references.push(neededSig)
} else {
// 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 { } else {
throw new Error( throw new Error(
'Implement inexact self-reference in typed-function for ' 'Implement inexact self-reference in typed-function for '
+ `${name}(${neededSig})`) + `${name}(${neededSig})`)
} }
} }
const refs = deferral.builtRefs }
const does = deferral.sigDoes const does = deferral.sigDoes
if (remaining_self_references.length > 0) {
imps[aSignature] = this._typed.referTo( imps[aSignature] = this._typed.referTo(
...corrected_self_references, (...impls) => { ...corrected_self_references, (...impls) => {
for (let i = 0; i < part_self_references.length; ++i) { for (let i = 0; i < remaining_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i] refs[`self(${remaining_self_references[i]})`] = impls[i]
} }
const implementation = does(refs) const implementation = does(refs)
// What will we do with the return type info in here? // What will we do with the return type info in here?
return implementation return implementation
} }
) )
} else {
imps[aSignature] = does(refs)
}
imps[aSignature]._pocoSignature = deferral._pocoSignature imps[aSignature]._pocoSignature = deferral._pocoSignature
imps[aSignature]._pocoInstance = deferral._pocoInstance imps[aSignature]._pocoInstance = deferral._pocoInstance
} }
@ -1247,7 +1383,7 @@ export default class PocomathInstance {
if (wantsType in this.Types) return false if (wantsType in this.Types) return false
// OK, need to generate the type from the template // OK, need to generate the type from the template
// Set up refines, before, test, and from // Set up refines, before, test, and from
const newTypeSpec = {refines: base} const newTypeSpec = {}
const maybeFrom = {} const maybeFrom = {}
const template = this.Templates[base].spec const template = this.Templates[base].spec
if (!template) { if (!template) {
@ -1255,6 +1391,11 @@ export default class PocomathInstance {
`Implementor error in instantiateTemplate(${base}, ${instantiator})`) `Implementor error in instantiateTemplate(${base}, ${instantiator})`)
} }
const instantiatorSpec = this.Types[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 = [] let beforeTypes = []
if (instantiatorSpec.before) { if (instantiatorSpec.before) {
beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`)
@ -1268,18 +1409,15 @@ export default class PocomathInstance {
if (beforeTypes.length > 0) { if (beforeTypes.length > 0) {
newTypeSpec.before = beforeTypes 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) { if (template.from) {
for (let source in template.from) { for (let source in template.from) {
const instSource = substituteInSignature( const instSource = substituteInSignature(
source, theTemplateParam, instantiator) source, theTemplateParam, instantiator)
let usesFromParam = false const testSource = substituteInSignature(
for (const word of instSource.split(/[<>]/)) { instSource, templateFromParam, instantiator)
if (word === templateFromParam) { const usesFromParam = (testSource !== instSource)
usesFromParam = true
break
}
}
if (usesFromParam) { if (usesFromParam) {
for (const iFrom in instantiatorSpec.from) { for (const iFrom in instantiatorSpec.from) {
const finalSource = substituteInSignature( const finalSource = substituteInSignature(
@ -1287,12 +1425,14 @@ export default class PocomathInstance {
maybeFrom[finalSource] = template.from[source]( maybeFrom[finalSource] = template.from[source](
instantiatorSpec.from[iFrom]) instantiatorSpec.from[iFrom])
} }
if (testSource !== wantsType) { // subtypes handled with refines
// Assuming all templates are covariant here, I guess... // Assuming all templates are covariant here, I guess...
for (const subType of this._subtypes[instantiator]) { for (const subType of this._subtypes[instantiator]) {
const finalSource = substituteInSignature( const finalSource = substituteInSignature(
instSource, templateFromParam, subType) instSource, templateFromParam, subType)
maybeFrom[finalSource] = template.from[source](x => x) maybeFrom[finalSource] = template.from[source](x => x)
} }
}
} else { } else {
maybeFrom[instSource] = template.from[source] maybeFrom[instSource] = template.from[source]
} }
@ -1303,6 +1443,7 @@ export default class PocomathInstance {
newTypeSpec.from = maybeFrom newTypeSpec.from = maybeFrom
} }
this.installType(wantsType, newTypeSpec) this.installType(wantsType, newTypeSpec)
this._instantiationsOf[base].add(wantsType)
return wantsType return wantsType
}) })
@ -1314,7 +1455,7 @@ export default class PocomathInstance {
const otherTypeList = typeListOfSignature(otherSig) const otherTypeList = typeListOfSignature(otherSig)
if (typeList.length !== otherTypeList.length) continue if (typeList.length !== otherTypeList.length) continue
let allMatch = true let allMatch = true
let paramBound = 'ground' let paramBound = 'any'
for (let k = 0; k < typeList.length; ++k) { for (let k = 0; k < typeList.length; ++k) {
let myType = typeList[k] let myType = typeList[k]
let otherType = otherTypeList[k] let otherType = otherTypeList[k]
@ -1326,8 +1467,7 @@ export default class PocomathInstance {
otherTypeList[k] = `...${paramBound}` otherTypeList[k] = `...${paramBound}`
otherType = paramBound otherType = paramBound
} }
const adjustedOtherType = otherType.replaceAll( const adjustedOtherType = otherType.replaceAll(templateCall, '')
`<${theTemplateParam}>`, '')
if (adjustedOtherType !== otherType) { if (adjustedOtherType !== otherType) {
otherTypeList[k] = adjustedOtherType otherTypeList[k] = adjustedOtherType
otherType = adjustedOtherType otherType = adjustedOtherType
@ -1342,22 +1482,21 @@ export default class PocomathInstance {
theTemplateParam, paramBound) theTemplateParam, paramBound)
} }
if (otherType === 'any') continue if (otherType === 'any') continue
if (otherType === 'ground') continue if (myType === otherType) continue
if (!(otherType in this.Types)) {
allMatch = false
break
}
if (myType === otherType
|| this.isSubtypeOf(myType, otherType)) {
continue
}
if (otherType in this.Templates) { if (otherType in this.Templates) {
const myBase = myType.split('<',1)[0]
if (myBase === otherType) continue
if (this.instantiateTemplate(otherType, myType)) { if (this.instantiateTemplate(otherType, myType)) {
let dummy let dummy
dummy = this[name] // for side effects dummy = this[name] // for side effects
return this._findSubtypeImpl(name, this._imps[name], neededSig) return this._findSubtypeImpl(name, this._imps[name], neededSig)
} }
} }
if (!(otherType in this.Types)) {
allMatch = false
break
}
if (this.isSubtypeOf(myType, otherType)) continue
allMatch = false allMatch = false
break break
} }
@ -1373,31 +1512,16 @@ export default class PocomathInstance {
if (!this._typed.isTypedFunction(typedFunction)) { if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name] typedFunction = this[name]
} }
let result = undefined
const haveTF = this._typed.isTypedFunction(typedFunction) const haveTF = this._typed.isTypedFunction(typedFunction)
if (haveTF) { if (haveTF) {
// First try a direct match
let result
try { try {
result = this._typed.findSignature(typedFunction, sig, {exact: true}) result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch { } catch {
} }
} if (result) return result
if (result || !(this._imps[name])) return result // Next, look ourselves but with subtypes:
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) {
if (haveTF) {
return this._typed.findSignature(typedFunction, foundsig)
}
// We have an implementation but not a typed function. Do the best
// we can:
const foundImpl = this._imps[name][foundsig]
const needs = {}
for (const dep of foundImpl.uses) {
const [base, sig] = dep.split('()')
needs[dep] = this.resolve(base, sig)
}
const pseudoImpl = foundImpl.does(needs)
return {fn: pseudoImpl, implementation: pseudoImpl}
}
const wantTypes = typeListOfSignature(sig) const wantTypes = typeListOfSignature(sig)
for (const [implSig, details] for (const [implSig, details]
of typedFunction._typedFunctionData.signatureMap) { of typedFunction._typedFunctionData.signatureMap) {
@ -1411,7 +1535,33 @@ export default class PocomathInstance {
} }
if (allMatched) return details if (allMatched) return details
} }
}
if (!(this._imps[name])) return undefined
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) {
if (haveTF) {
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:
const foundImpl = this._imps[name][foundsig]
const needs = {}
for (const dep of foundImpl.uses) {
const [base, sig] = dep.split('()')
needs[dep] = this.resolve(base, sig)
}
const pseudoImpl = foundImpl.does(needs)
return {fn: pseudoImpl, implementation: pseudoImpl}
}
// Hmm, no luck. Make sure bundle is up-to-date and retry: // Hmm, no luck. Make sure bundle is up-to-date and retry:
let result = undefined
typedFunction = this[name] typedFunction = this[name]
try { try {
result = this._typed.findSignature(typedFunction, sig) result = this._typed.findSignature(typedFunction, sig)

View File

@ -1,7 +1,9 @@
import Returns from '../core/Returns.mjs'
export const abs = { export const abs = {
T: ({ T: ({
T,
'smaller(T,T)': lt, 'smaller(T,T)': lt,
'negate(T)': neg, 'negate(T)': neg,
'zero(T)': zr '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 * 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' import PocomathInstance from '../../core/PocomathInstance.mjs'
const Tuple = new PocomathInstance('Tuple') 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>', { Tuple.installType('Tuple<T>', {
// We are assuming that any 'Type<T>' refines 'Type', so this is // A test that "defines" the "base type", which is not really a type
// not necessary: // (only fully instantiated types are added to the universe)
// refines: 'Tuple', base: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts),
// But we need there to be a way to determine the type of a tuple: // The template portion of the test; it takes the test for T as
infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), // input and returns the test for an entity _that already passes
// For the test, we can assume that t is already a base tuple, // the base test_ to be a Tuple<T>:
// and we get the test for T as an input and we have to return
// the test for Tuple<T>
test: testT => t => t.elts.every(testT), test: testT => t => t.elts.every(testT),
// These are only invoked for types U such that there is already // And we need there to be a way to determine the (instantiation)
// a conversion from U to T, and that conversion is passed as an input // type of an tuple (that has already passed the base test):
// and we have to return the conversion to Tuple<T>: 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: { from: {
'Tuple<U>': convert => tu => ({elts: tu.elts.map(convert)}), 'Tuple<U>': convert => tu => ({elts: tu.elts.map(convert)}),
// Here since there is no U it's a straight conversion: // 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 {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('identity', 'Fraction'), 'Fraction')
assert.strictEqual( assert.strictEqual(
math.returnTypeOf('quotient', 'bigint,bigint'), 'bigint') 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', () => { it('can subtract numbers', () => {

View File

@ -135,13 +135,8 @@ describe('A custom instance', () => {
assert.strictEqual( assert.strictEqual(
inst.typeMerge(3, inst.complex(4.5,2.1)), inst.typeMerge(3, inst.complex(4.5,2.1)),
'Merge to Complex<number>') 'Merge to Complex<number>')
// The following is the current behavior, since 3 converts to 3+0i assert.throws(
// which is technically the same Complex type as 3n+0ni. () => inst.typeMerge(3, inst.complex(3n)), TypeError)
// 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...
inst.install(genericSubtract) inst.install(genericSubtract)
assert.throws(() => inst.typeMerge(3, undefined), TypeError) assert.throws(() => inst.typeMerge(3, undefined), TypeError)
}) })

View File

@ -8,14 +8,12 @@ describe('tuple', () => {
it('does not allow unification by converting consecutive arguments', () => { it('does not allow unification by converting consecutive arguments', () => {
assert.throws(() => math.tuple(3, 5.2, 2n), /TypeError.*unif/) 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( assert.throws(
() => math.tuple(3, 2n, math.complex(5.2)), () => math.tuple(3, 2n, math.complex(5.2)),
/TypeError.*unif/) /TypeError.*unif/)
assert.deepStrictEqual( assert.throws(
math.tuple(3, math.complex(2n), 5.2), () => math.tuple(3, math.complex(2n), 5.2),
{elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) /TypeError.*unif/)
}) })
it('can be tested for zero and equality', () => { it('can be tested for zero and equality', () => {