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
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<T>' 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) {
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 (needsig && self._typed.isTypedFunction(refs[dep])) {
if (self._typed.isTypedFunction(refs[simplifiedDep])) {
const subsig = substituteInSig(
needsig, theTemplateParam, instantiateFor)
if (subsig !== needsig) {
refs[dep] = self._typed.find(refs[dep], subsig)
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]
}
}
}

View File

@ -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
})

View File

@ -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
}

View File

@ -8,13 +8,11 @@ Tuple.installType('Tuple', {
})
// Now the template type that is the primary use of this
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:
// 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<T>

View File

@ -1,6 +1,8 @@
export {Tuple} from './Types/Tuple.mjs'
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
* 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', () => {
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)
})
})