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:
parent
a743337134
commit
b0fb004224
@ -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) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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`
|
||||
}
|
||||
|
||||
|
@ -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})}
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user