feat: Template types for Pocomath

Tuple<T> 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.
This commit is contained in:
Glen Whitney 2022-08-04 08:23:14 -07:00
parent a743337134
commit b0fb004224
7 changed files with 190 additions and 44 deletions

View File

@ -184,6 +184,9 @@ export default class PocomathInstance {
if (type === 'any' || this._templateParam(type)) continue if (type === 'any' || this._templateParam(type)) continue
this.installType(type, spec) this.installType(type, spec)
} }
for (const [base, info] of Object.entries(other._templateTypes)) {
this._installTemplateType(info.type, info.spec)
}
const migrateImps = {} const migrateImps = {}
for (const operator in other._imps) { for (const operator in other._imps) {
if (operator != 'typeOf') { // skip the builtin, we already have it if (operator != 'typeOf') { // skip the builtin, we already have it
@ -380,6 +383,7 @@ export default class PocomathInstance {
_joinTypes(typeA, typeB, convert) { _joinTypes(typeA, typeB, convert) {
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 === typeB) return typeA if (typeA === typeB) return typeA
const subber = convert ? this._priorTypes : this._subtypes const subber = convert ? this._priorTypes : this._subtypes
if (subber[typeB].has(typeA)) return typeB if (subber[typeB].has(typeA)) return typeB
@ -612,12 +616,13 @@ export default class PocomathInstance {
} }
return behavior.does(innerRefs) 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 */ /* Now add the catchall signature */
let templateCall = `<${theTemplateParam}>` let templateCall = `<${theTemplateParam}>`
/* Relying here that the base of 'Foo<T>' is 'Foo': */ /* Relying here that the base of 'Foo<T>' is 'Foo': */
let baseSignature = substituteInSig(trimSignature, templateCall, '') let baseSignature = trimSignature.replaceAll(templateCall, '')
/* Any remaining template params are top-level */ /* Any remaining template params are top-level */
const signature = substituteInSig( const signature = substituteInSig(
baseSignature, theTemplateParam, 'any') baseSignature, theTemplateParam, 'any')
@ -626,6 +631,7 @@ export default class PocomathInstance {
* First, prepare the type inference data: * First, prepare the type inference data:
*/ */
const parTypes = trimSignature.split(',') const parTypes = trimSignature.split(',')
const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...')
const topTyper = entity => this.typeOf(entity) const topTyper = entity => this.typeOf(entity)
const inferences = parTypes.map( const inferences = parTypes.map(
type => generateTypeExtractor( type => generateTypeExtractor(
@ -638,10 +644,31 @@ export default class PocomathInstance {
throw new SyntaxError( throw new SyntaxError(
`Cannot find template parameter in ${rawSignature}`) `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 */ /* Now build the catchall implementation */
const self = this const self = this
const patch = (refs) => (...args) => { 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 i = -1
let j = -1 let j = -1
/* collect the arg types */ /* collect the arg types */
@ -652,10 +679,15 @@ export default class PocomathInstance {
if (i < inferences.length - 1) ++i if (i < inferences.length - 1) ++i
if (inferences[i]) { if (inferences[i]) {
const argType = inferences[i](arg) const argType = inferences[i](arg)
if (!argType || argType === 'any') { if (!argType) {
throw TypeError( throw TypeError(
`Type inference failed for argument ${j} of ${name}`) `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) argTypes.push(argType)
} }
} }
@ -669,7 +701,11 @@ export default class PocomathInstance {
instantiateFor = self.joinTypes(argTypes, usedConversions) instantiateFor = self.joinTypes(argTypes, usedConversions)
if (instantiateFor === 'any') { if (instantiateFor === 'any') {
// Need a more informative error message here // 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 */ /* 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" * 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[i] !== parTypes[i] && wantTypes.includes('<')) { if (wantTypes[j] !== parTypes[j] && wantTypes[j].includes('<')) {
// actually used the param and is a template // 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: */ /* Transform the arguments if we used any conversions: */
@ -691,13 +727,23 @@ export default class PocomathInstance {
i = - 1 i = - 1
for (j = 0; j < args.length; ++j) { for (j = 0; j < args.length; ++j) {
if (i < parTypes.length - 1) ++i if (i < parTypes.length - 1) ++i
const wantType = substituteInSig( let wantType = parTypes[i]
parTypes[i], theTemplateParam, instantiateFor) if (wantType.slice(0,3) === '...') {
wantType = wantType.slice(3)
}
wantType = substituteInSig(
wantType, theTemplateParam, instantiateFor)
if (wantType !== parTypes[i]) { if (wantType !== parTypes[i]) {
args[j] = self._typed.convert(args[j], wantType) 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 /* Arrange that the desired instantiation will be there next
* time so we don't have to go through that again for this type * 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) self._invalidate(name)
// And update refs because we now know the type we're instantiating // And update refs because we now know the type we're instantiating
// for: // for:
for (const dep of behavior.uses) { const innerRefs = {}
for (const dep in simplifiedUses) {
const simplifiedDep = simplifiedUses[dep]
if (dep === simplifiedDep) {
innerRefs[dep] = refs[dep]
} else {
let [func, needsig] = dep.split(/[()]/) let [func, needsig] = dep.split(/[()]/)
if (needsig && self._typed.isTypedFunction(refs[dep])) { if (self._typed.isTypedFunction(refs[simplifiedDep])) {
const subsig = substituteInSig( const subsig = substituteInSig(
needsig, theTemplateParam, instantiateFor) needsig, theTemplateParam, instantiateFor)
if (subsig !== needsig) { innerRefs[dep] = self._typed.find(
refs[dep] = self._typed.find(refs[dep], subsig) refs[simplifiedDep], subsig)
} else {
innerRefs[dep] = refs[simplifiedDep]
} }
} }
} }
// Finally ready to make the call. // 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( 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) const tf = this._typed(name, tf_imps)
Object.defineProperty(this, name, {configurable: true, value: tf}) Object.defineProperty(this, name, {configurable: true, value: tf})
return tf return tf
@ -742,9 +798,17 @@ export default class PocomathInstance {
let part_self_references = [] let part_self_references = []
for (const dep of uses) { for (const dep of uses) {
let [func, needsig] = dep.split(/[()]/) let [func, needsig] = dep.split(/[()]/)
const needTypes = needsig ? typesOfSignature(needsig) : new Set() /* Safety check that can perhaps be removed:
/* For now, punt on template parameters */ * Verify that the desired signature has been fully grounded:
if (needTypes.has(theTemplateParam)) needsig = '' */
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 (func === 'self') {
if (needsig) { if (needsig) {
if (full_self_referential) { if (full_self_referential) {
@ -791,17 +855,77 @@ export default class PocomathInstance {
return return
} }
if (part_self_references.length) { if (part_self_references.length) {
imps[signature] = this._typed.referTo( /* There is an obstruction here. The list part_self_references
...part_self_references, (...impls) => { * 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) { for (let i = 0; i < part_self_references.length; ++i) {
refs[`self(${part_self_references[i]})`] = impls[i] refs[`self(${part_self_references[i]})`] = impls[i]
} }
return does(refs) return does(refs)
} }
) )
return
} }
imps[signature] = does(refs)
} }
/* This function analyzes the template and makes sure the /* This function analyzes the template and makes sure the
@ -817,7 +941,7 @@ export default class PocomathInstance {
} }
let instantiations let instantiations
if (this._templateParam(arg)) { // 1st-level template if (this._templateParam(arg)) { // 1st-level template
instantiations = new Set(this._priorTypes(type)) instantiations = new Set(this._priorTypes[type])
instantiations.add(type) instantiations.add(type)
} else { // nested template } else { // nested template
instantiations = this._ensureTemplateTypes(arg, type) instantiations = this._ensureTemplateTypes(arg, type)
@ -825,7 +949,7 @@ export default class PocomathInstance {
const resultingTypes = new Set() const resultingTypes = new Set()
for (const iType of instantiations) { for (const iType of instantiations) {
const resultType = this._maybeAddTemplateType(base, iType) const resultType = this._maybeAddTemplateType(base, iType)
if (resultType) resultingTypes.push(resultType) if (resultType) resultingTypes.add(resultType)
} }
return resultingTypes return resultingTypes
} }
@ -840,7 +964,7 @@ export default class PocomathInstance {
// 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 = {} const newTypeSpec = {}
const template = this._templateTypes[base] const template = this._templateTypes[base].spec
if (!template) { if (!template) {
throw new Error( throw new Error(
`Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`)
@ -848,6 +972,7 @@ export default class PocomathInstance {
const instantiatorSpec = this.Types[instantiator] const instantiatorSpec = this.Types[instantiator]
if (instantiatorSpec.refines) { if (instantiatorSpec.refines) {
// Assuming all templates are covariant, for now // Assuming all templates are covariant, for now
this._maybeAddTemplateType(base, instantiatorSpec.refines)
newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>`
} }
let beforeTypes = [] let beforeTypes = []
@ -867,9 +992,10 @@ export default class PocomathInstance {
if (template.from) { if (template.from) {
newTypeSpec.from = {} newTypeSpec.from = {}
for (let source in template.from) { for (let source in template.from) {
source = substituteInSig(source, theTemplateParam, instantiator) const instSource = substituteInSig(
const usesFromParam = false source, theTemplateParam, instantiator)
for (const word of source.split(/[<>]/)) { let usesFromParam = false
for (const word of instSource.split(/[<>]/)) {
if (word === templateFromParam) { if (word === templateFromParam) {
usesFromParam = true usesFromParam = true
break break
@ -878,12 +1004,12 @@ export default class PocomathInstance {
if (usesFromParam) { if (usesFromParam) {
for (const iFrom in instantiatorSpec.from) { for (const iFrom in instantiatorSpec.from) {
const finalSource = substituteInSig( const finalSource = substituteInSig(
source, templateFromParam, iFrom) instSource, templateFromParam, iFrom)
newTypeSpec[finalSource] = template.from[source]( newTypeSpec.from[finalSource] = template.from[source](
instantiatorSpec.from[iFrom]) instantiatorSpec.from[iFrom])
} }
} else { } else {
newTypeSpec[source] = template.from[source] newTypeSpec.from[instSource] = template.from[source]
} }
} }
} }

View File

@ -32,9 +32,9 @@ export function generateTypeExtractor(
if (!(base in templates)) return false // unknown template if (!(base in templates)) return false // unknown template
const arg = type.slice(base.length+1, -1) const arg = type.slice(base.length+1, -1)
const argExtractor = generateTypeExtractor( const argExtractor = generateTypeExtractor(
arg, param, topTyper, typeJointer, templates) arg, param, topTyper, typeJoiner, templates)
if (!argExtractor) return false if (!argExtractor) return false
return templates[base].infer({ return templates[base].spec.infer({
typeOf: argExtractor, typeOf: argExtractor,
joinTypes: typeJoiner joinTypes: typeJoiner
}) })

View File

@ -8,6 +8,7 @@ import * as tuple from './tuple/native.mjs'
const tupleReady = { const tupleReady = {
Tuple: tuple.Tuple, Tuple: tuple.Tuple,
equal: tuple.equal, equal: tuple.equal,
isZero: tuple.isZero,
length: tuple.length, length: tuple.length,
tuple: tuple.tuple tuple: tuple.tuple
} }

View File

@ -8,13 +8,11 @@ Tuple.installType('Tuple', {
}) })
// Now the template type that is the primary use of this // Now the template type that is the primary use of this
Tuple.installType('Tuple<T>', { Tuple.installType('Tuple<T>', {
// For now we will assume that any 'Type<T>' refines 'Type', so this is // We are assuming that any 'Type<T>' refines 'Type', so this is
// not necessary: // not necessary:
// refines: 'Tuple', // refines: 'Tuple',
// But we need there to be a way to determine the type of a tuple: // But we need there to be a way to determine the type of a tuple:
infer: ({typeOf, joinTypes}) => t => { infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)),
return joinTypes(t.elts.map(typeOf))
},
// For the test, we can assume that t is already a base tuple, // 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 // and we get the test for T as an input and we have to return
// the test for Tuple<T> // the test for Tuple<T>

View File

@ -1,6 +1,8 @@
export {Tuple} from './Types/Tuple.mjs' export {Tuple} from './Types/Tuple.mjs'
export const isZero = { export const isZero = {
'Tuple<T>': ({'self(T)': me}) => t => t.elts.every(isZero) 'Tuple<T>': ({'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`
} }

View File

@ -3,4 +3,4 @@ export {Tuple} from './Types/Tuple.mjs'
/* The purpose of the template argument is to ensure that all of the args /* The purpose of the template argument is to ensure that all of the args
* are convertible to the same type. * are convertible to the same type.
*/ */
export const tuple = {'...any': () => args => ({elts: args})} export const tuple = {'...T': () => args => ({elts: args})}

View File

@ -3,7 +3,26 @@ import math from '../../src/pocomath.mjs'
describe('tuple', () => { describe('tuple', () => {
it('can be created and provide its length', () => { 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)
}) })
}) })