From de42c22ab4af1c86eafda38acd80e10b22fa3cf7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 09:30:07 -0400 Subject: [PATCH] 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. --- src/complex/Types/Complex.mjs | 10 +- src/complex/abs.mjs | 1 + src/complex/associate.mjs | 5 +- src/complex/multiply.mjs | 10 +- src/core/PocomathInstance.mjs | 616 +++++++++++++++++++++------------- src/generic/abs.mjs | 4 +- src/number/abs.mjs | 3 +- src/tuple/Types/Tuple.mjs | 30 +- src/tuple/length.mjs | 3 +- test/_pocomath.mjs | 3 + test/complex/_all.mjs | 4 +- test/custom.mjs | 9 +- test/tuple/_native.mjs | 8 +- 13 files changed, 428 insertions(+), 278 deletions(-) diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 6dbaa60..4d63417 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -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', { - 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 => { diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index d793d33..30034ed 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -3,6 +3,7 @@ export * from './Types/Complex.mjs' export const abs = { 'Complex': ({ + T, sqrt, // Unfortunately no notation yet for the needed signature 'absquare(T)': baseabsq, 'absquare(Complex)': absq diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs index 10c799c..9aae6e1 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -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)': 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)) - } + }) } diff --git a/src/complex/multiply.mjs b/src/complex/multiply.mjs index db59a44..5d9edc6 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -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) + } ) } diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 25e3d18..f591941 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -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', 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' 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) diff --git a/src/generic/abs.mjs b/src/generic/abs.mjs index 84ebc31..ff20ee8 100644 --- a/src/generic/abs.mjs +++ b/src/generic/abs.mjs @@ -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)) } diff --git a/src/number/abs.mjs b/src/number/abs.mjs index 66ede16..80b45d8 100644 --- a/src/number/abs.mjs +++ b/src/number/abs.mjs @@ -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))} diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index d76d147..49255f7 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -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', { - // We are assuming that any 'Type' 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 + // 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: 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: + // 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: from: { 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), // Here since there is no U it's a straight conversion: diff --git a/src/tuple/length.mjs b/src/tuple/length.mjs index f3e8f2d..4df2c74 100644 --- a/src/tuple/length.mjs +++ b/src/tuple/length.mjs @@ -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': () => Returns('NumInt', t => t.elts.length)} diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index b49efb6..549e6ba 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -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'), 'number') }) it('can subtract numbers', () => { diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 884350a..413503c 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -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))) }) diff --git a/test/custom.mjs b/test/custom.mjs index f6612ef..b6fd111 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -135,13 +135,8 @@ describe('A custom instance', () => { assert.strictEqual( inst.typeMerge(3, inst.complex(4.5,2.1)), 'Merge to Complex') - // 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) }) diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 2cf56d1..a37b213 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -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', () => {