From b0fb00422434311a5d1130c72eb24d891a35e7f1 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 4 Aug 2022 08:23:14 -0700 Subject: [PATCH] feat: Template types for Pocomath Tuple and a couple of functions on it are now working according to the target spec. As desired, no instantiations of a template type are made until some function that takes an instantiation is called. --- src/core/PocomathInstance.mjs | 194 ++++++++++++++++++++++++++++------ src/core/extractors.mjs | 4 +- src/pocomath.mjs | 1 + src/tuple/Types/Tuple.mjs | 6 +- src/tuple/isZero.mjs | 6 +- src/tuple/tuple.mjs | 2 +- test/tuple/_native.mjs | 21 +++- 7 files changed, 190 insertions(+), 44 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 0253de0..cc999c1 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -184,6 +184,9 @@ export default class PocomathInstance { if (type === 'any' || this._templateParam(type)) continue this.installType(type, spec) } + for (const [base, info] of Object.entries(other._templateTypes)) { + this._installTemplateType(info.type, info.spec) + } const migrateImps = {} for (const operator in other._imps) { if (operator != 'typeOf') { // skip the builtin, we already have it @@ -380,6 +383,7 @@ export default class PocomathInstance { _joinTypes(typeA, typeB, convert) { if (!typeA) return typeB if (!typeB) return typeA + if (typeA === 'any' || typeB === 'any') return 'any' if (typeA === typeB) return typeA const subber = convert ? this._priorTypes : this._subtypes if (subber[typeB].has(typeA)) return typeB @@ -612,12 +616,13 @@ export default class PocomathInstance { } return behavior.does(innerRefs) } - this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + this._addTFimplementation( + tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = substituteInSig(trimSignature, templateCall, '') + let baseSignature = trimSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSig( baseSignature, theTemplateParam, 'any') @@ -626,6 +631,7 @@ export default class PocomathInstance { * First, prepare the type inference data: */ const parTypes = trimSignature.split(',') + const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') const topTyper = entity => this.typeOf(entity) const inferences = parTypes.map( type => generateTypeExtractor( @@ -638,10 +644,31 @@ export default class PocomathInstance { throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } + /* And eliminate template parameters from the dependencies */ + const simplifiedUses = {} + for (const dep of behavior.uses) { + let [func, needsig] = dep.split(/[()]/) + if (needsig) { + const subsig = substituteInSig(needsig, theTemplateParam, '') + if (subsig === needsig) { + simplifiedUses[dep] = dep + } else { + simplifiedUses[dep] = func + } + } else { + simplifiedUses[dep] = dep + } + } /* Now build the catchall implementation */ const self = this const patch = (refs) => (...args) => { - /* First infer the type we actually should have been called for */ + /* 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 */ @@ -652,10 +679,15 @@ export default class PocomathInstance { if (i < inferences.length - 1) ++i if (inferences[i]) { const argType = inferences[i](arg) - if (!argType || argType === 'any') { + 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(', ')) + } argTypes.push(argType) } } @@ -669,7 +701,11 @@ export default class PocomathInstance { instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { // Need a more informative error message here - throw TypeError('No common type for arguments to ' + name) + 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') } } /* Generate the list of actual wanted types */ @@ -681,9 +717,9 @@ export default class PocomathInstance { * by instantiateFor, and for all of instantiateFor's "prior types" */ for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[i] !== parTypes[i] && wantTypes.includes('<')) { + if (wantTypes[j] !== parTypes[j] && wantTypes[j].includes('<')) { // actually used the param and is a template - self._ensureTemplateTypes(parTypes[i], instantiateFor) + self._ensureTemplateTypes(parTypes[j], instantiateFor) } } /* Transform the arguments if we used any conversions: */ @@ -691,13 +727,23 @@ export default class PocomathInstance { i = - 1 for (j = 0; j < args.length; ++j) { if (i < parTypes.length - 1) ++i - const wantType = substituteInSig( - parTypes[i], theTemplateParam, instantiateFor) + let wantType = parTypes[i] + if (wantType.slice(0,3) === '...') { + wantType = wantType.slice(3) + } + wantType = substituteInSig( + 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 */ @@ -706,22 +752,32 @@ export default class PocomathInstance { self._invalidate(name) // And update refs because we now know the type we're instantiating // for: - for (const dep of behavior.uses) { - let [func, needsig] = dep.split(/[()]/) - if (needsig && self._typed.isTypedFunction(refs[dep])) { - const subsig = substituteInSig( - needsig, theTemplateParam, instantiateFor) - if (subsig !== needsig) { - refs[dep] = self._typed.find(refs[dep], subsig) + 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 = substituteInSig( + needsig, theTemplateParam, instantiateFor) + innerRefs[dep] = self._typed.find( + refs[simplifiedDep], subsig) + } else { + innerRefs[dep] = refs[simplifiedDep] } } } // Finally ready to make the call. - return behavior.does(refs)(...args) + return behavior.does(innerRefs)(...args) } + // The actual uses value needs to be a set: + const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( - tf_imps, signature, {uses: behavior.uses, does: patch}) + tf_imps, signature, {uses: outerUses, does: patch}) } + this._correctPartialSelfRefs(tf_imps) const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf @@ -742,9 +798,17 @@ export default class PocomathInstance { let part_self_references = [] for (const dep of uses) { let [func, needsig] = dep.split(/[()]/) - const needTypes = needsig ? typesOfSignature(needsig) : new Set() - /* For now, punt on template parameters */ - if (needTypes.has(theTemplateParam)) needsig = '' + /* Safety check that can perhaps be removed: + * Verify that the desired signature has been fully grounded: + */ + if (needsig) { + const trysig = substituteInSig(needsig, theTemplateParam, '') + if (trysig !== needsig) { + throw new Error( + 'Attempt to add a template implementation: ' + + `${signature} with dependency ${dep}`) + } + } if (func === 'self') { if (needsig) { if (full_self_referential) { @@ -791,17 +855,77 @@ export default class PocomathInstance { return } if (part_self_references.length) { - imps[signature] = this._typed.referTo( - ...part_self_references, (...impls) => { + /* There is an obstruction here. The list part_self_references + * might contain a signature that requires conversion for self to + * handle. But I advocated this not be allowed in typed.referTo, which + * made sense for human-written functions, but is unfortunate now. + * So we have to defer creating these and correct them later, at + * least until we can add an option to typed-function. + */ + imps[signature] = { + deferred: true, + builtRefs: refs, + sigDoes: does, + psr: part_self_references + } + return + } + imps[signature] = does(refs) + } + + _correctPartialSelfRefs(imps) { + for (const aSignature in imps) { + if (!(imps[aSignature].deferred)) continue + const part_self_references = imps[aSignature].psr + const corrected_self_references = [] + 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) + continue + } + // No exact match, have to try to get one that matches with + // subtypes since the whole conversion thing in typed-function + // is too complicated to reproduce + let foundSig = false + const typeList = typesOfSignature(neededSig) + for (const otherSig in imps) { + const otherTypeList = typesOfSignature(otherSig) + if (typeList.length !== otherTypeList.length) continue + const allMatch = true + for (let k = 0; k < typeList.length; ++k) { + if (this._subtypes[otherTypeList[k]].has(typeList[k])) { + continue + } + allMatch = false + break + } + if (allMatch) { + foundSig = otherSig + break + } + } + if (foundSig) { + corrected_self_references.push(foundSig) + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + neededSig) + } + } + const refs = imps[aSignature].builtRefs + const does = imps[aSignature].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] } return does(refs) } ) - return } - imps[signature] = does(refs) } /* This function analyzes the template and makes sure the @@ -817,7 +941,7 @@ export default class PocomathInstance { } let instantiations if (this._templateParam(arg)) { // 1st-level template - instantiations = new Set(this._priorTypes(type)) + instantiations = new Set(this._priorTypes[type]) instantiations.add(type) } else { // nested template instantiations = this._ensureTemplateTypes(arg, type) @@ -825,7 +949,7 @@ export default class PocomathInstance { const resultingTypes = new Set() for (const iType of instantiations) { const resultType = this._maybeAddTemplateType(base, iType) - if (resultType) resultingTypes.push(resultType) + if (resultType) resultingTypes.add(resultType) } return resultingTypes } @@ -840,7 +964,7 @@ export default class PocomathInstance { // OK, need to generate the type from the template // Set up refines, before, test, and from const newTypeSpec = {} - const template = this._templateTypes[base] + const template = this._templateTypes[base].spec if (!template) { throw new Error( `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) @@ -848,6 +972,7 @@ export default class PocomathInstance { const instantiatorSpec = this.Types[instantiator] if (instantiatorSpec.refines) { // Assuming all templates are covariant, for now + this._maybeAddTemplateType(base, instantiatorSpec.refines) newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` } let beforeTypes = [] @@ -867,9 +992,10 @@ export default class PocomathInstance { if (template.from) { newTypeSpec.from = {} for (let source in template.from) { - source = substituteInSig(source, theTemplateParam, instantiator) - const usesFromParam = false - for (const word of source.split(/[<>]/)) { + const instSource = substituteInSig( + source, theTemplateParam, instantiator) + let usesFromParam = false + for (const word of instSource.split(/[<>]/)) { if (word === templateFromParam) { usesFromParam = true break @@ -878,12 +1004,12 @@ export default class PocomathInstance { if (usesFromParam) { for (const iFrom in instantiatorSpec.from) { const finalSource = substituteInSig( - source, templateFromParam, iFrom) - newTypeSpec[finalSource] = template.from[source]( + instSource, templateFromParam, iFrom) + newTypeSpec.from[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } } else { - newTypeSpec[source] = template.from[source] + newTypeSpec.from[instSource] = template.from[source] } } } diff --git a/src/core/extractors.mjs b/src/core/extractors.mjs index ff63d9d..0db3c0f 100644 --- a/src/core/extractors.mjs +++ b/src/core/extractors.mjs @@ -32,9 +32,9 @@ export function generateTypeExtractor( if (!(base in templates)) return false // unknown template const arg = type.slice(base.length+1, -1) const argExtractor = generateTypeExtractor( - arg, param, topTyper, typeJointer, templates) + arg, param, topTyper, typeJoiner, templates) if (!argExtractor) return false - return templates[base].infer({ + return templates[base].spec.infer({ typeOf: argExtractor, joinTypes: typeJoiner }) diff --git a/src/pocomath.mjs b/src/pocomath.mjs index 9081283..53e646f 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -8,6 +8,7 @@ import * as tuple from './tuple/native.mjs' const tupleReady = { Tuple: tuple.Tuple, equal: tuple.equal, + isZero: tuple.isZero, length: tuple.length, tuple: tuple.tuple } diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index a6dab25..7b4ed35 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -8,13 +8,11 @@ Tuple.installType('Tuple', { }) // Now the template type that is the primary use of this Tuple.installType('Tuple', { - // For now we will assume that any 'Type' refines 'Type', so this is + // 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 => { - return joinTypes(t.elts.map(typeOf)) - }, + 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 diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs index 1c3d9d2..9375277 100644 --- a/src/tuple/isZero.mjs +++ b/src/tuple/isZero.mjs @@ -1,6 +1,8 @@ export {Tuple} from './Types/Tuple.mjs' export const isZero = { - 'Tuple': ({'self(T)': me}) => t => t.elts.every(isZero) + 'Tuple': ({'self(T)': me}) => t => t.elts.every(e => me(e)) + // Note we can't just say `every(me)` above since every invokes its + // callback with more arguments, which then violates typed-function's + // signature for `me` } - diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs index a025028..893b54d 100644 --- a/src/tuple/tuple.mjs +++ b/src/tuple/tuple.mjs @@ -3,4 +3,4 @@ export {Tuple} from './Types/Tuple.mjs' /* The purpose of the template argument is to ensure that all of the args * are convertible to the same type. */ -export const tuple = {'...any': () => args => ({elts: args})} +export const tuple = {'...T': () => args => ({elts: args})} diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 3268cf6..7727ac9 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -3,7 +3,26 @@ import math from '../../src/pocomath.mjs' describe('tuple', () => { it('can be created and provide its length', () => { - assert.strictEqual(math.length(math.tuple(3,5.2,2n)), 3) + assert.strictEqual(math.length(math.tuple(3, 5.2, 2)), 3) + }) + + 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)]}) + }) + + it('can be tested for zero', () => { + assert.strictEqual(math.isZero(math.tuple(0,1)), false) + assert.strictEqual(math.isZero(math.tuple(0n,0n,0n,0n)), true) + assert.strictEqual(math.isZero(math.tuple(0,0.001,0)), false) + assert.strictEqual(math.isZero(math.tuple(0,math.complex(0,0))), true) }) })