feat: Instantiate instances of template instantiation need for self-reference

Formerly, when a self-reference like `self(Tuple<number>)` was encountered,
  but the `Tuple<number>` instance of a `Tuple<T>` implementation for this
  operation had not yet been instantiated, the reference would be fulfilled
  with a call to the catchall implementation for `Tuple`. Now the
  necessary instance is instantiated on the spot and referred to instead.

  This change is used to complete return-type specification for all of the
  Tuple functions.
This commit is contained in:
Glen Whitney 2022-08-30 15:15:23 -04:00
parent be9794fd4c
commit d83f2a7f23
8 changed files with 255 additions and 113 deletions

View File

@ -8,13 +8,8 @@ export const abs = {
'absquare(T)': baseabsq,
'absquare(Complex<T>)': absq
}) => {
const pm = sqrt.fromInstance
if (typeof pm === 'undefined') {
// Just checking for the dependencies, return value is irrelevant
return undefined
}
const midType = returnTypeOf(baseabsq)
const sqrtImp = pm.resolve('sqrt', midType, sqrt)
const sqrtImp = sqrt.fromInstance.resolve('sqrt', midType, sqrt)
let retType = returnTypeOf(sqrtImp)
if (retType.includes('|')) {
// This is a bit of a hack, as it relies on all implementations of

View File

@ -9,13 +9,9 @@ export const absquare = {
// we extract the needed implementation below.
'self(T)': absq
}) => {
const pm = add.fromInstance
if (typeof pm === 'undefined') {
// Just checking the dependencies, return value irrelevant
return undefined
}
const midType = returnTypeOf(absq)
const addImp = pm.resolve('add', `${midType},${midType}`, add)
const addImp = add.fromInstance.resolve(
'add', `${midType},${midType}`, add)
return Returns(
returnTypeOf(addImp), z => addImp(absq(z.re), absq(z.im)))
}

View File

@ -7,11 +7,76 @@ import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type
/* Template/signature parsing stuff; should probably be moved to a
* separate file, but it's a bit interleaved at the moment
*/
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 the pair [base, instance] for a template type. If the type
* is not a template, instance is undefined
*/
const templatePattern = /^\s*([^<\s]*)\s*<\s*(\S*)\s*>\s*$/
function splitTemplate(type) {
if (!(type.includes('<'))) return [type, undefined]
const results = templatePattern.exec(type)
return [results[1], results[2]]
}
/* Returns the instance such that type is template instantiated for that
* instance.
*/
function whichInstance(type, template) {
if (template === theTemplateParam) return type
if (type === template) return ''
if (!(template.includes(templateCall))) {
throw new TypeError(
`Type ${template} is not a template, so can't produce ${type}`)
}
const [typeBase, typeInstance] = splitTemplate(type)
if (!typeInstance) {
throw new TypeError(
`Type ${type} not from a template, so isn't instance of ${template}`)
}
const [tempBase, tempInstance] = splitTemplate(template)
if (typeBase !== tempBase) {
throw new TypeError(
`Type ${type} has wrong top-level base to be instance of ${template}`)
}
return whichInstance(typeInstance, tempInstance)
}
/* Same as above, but for signatures */
function whichSigInstance(sig, tempsig) {
const sigTypes = typeListOfSignature(sig)
const tempTypes = typeListOfSignature(tempsig)
const sigLength = sigTypes.length
if (sigLength === 0) {
throw new TypeError("No types in signature, so can't determine instance")
}
if (sigLength !== tempTypes.length) {
throw new TypeError(`Signatures ${sig} and ${tempsig} differ in length`)
}
let maybeInstance = whichInstance(sigTypes[0], tempTypes[0])
for (let i = 1; i < sigLength; ++i) {
const currInstance = whichInstance(sigTypes[i], tempTypes[i])
if (maybeInstance) {
if (currInstance && currInstance !== maybeInstance) {
throw new TypeError(
`Inconsistent instantiation of ${sig} from ${tempsig}`)
}
} else {
maybeInstance = currInstance
}
}
if (!maybeInstance) {
throw new TypeError(
`Signature ${sig} identical to ${tempsig}, not an instance`)
}
return maybeInstance
}
/* Returns a new signature just like sig but with the parameter replaced by
* the type
*/
@ -202,7 +267,10 @@ export default class PocomathInstance {
const stdimps = {}
for (const [signature, does] of Object.entries(spec)) {
const uses = new Set()
does(dependencyExtractor(uses))
try {
does(dependencyExtractor(uses))
} catch {
}
stdimps[signature] = {uses, does}
}
stdFunctions[item] = stdimps
@ -334,7 +402,7 @@ export default class PocomathInstance {
* the corresponding changes to the _typed object immediately
*/
installType = Returns('void', function(type, spec) {
const parts = type.split(/[<,>]/)
const parts = type.split(/[<,>]/).map(s => s.trim())
if (this._templateParam(parts[0])) {
throw new SyntaxError(
`Type name '${type}' reserved for template parameter`)
@ -400,14 +468,14 @@ export default class PocomathInstance {
}
/* 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]) {
const [toBase, toInstance] = splitTemplate(nextSuper)
if (toInstance) {
const [fromBase, fromInstance] = splitTemplate(from)
if (!fromBase || fromBase !== toBase) {
this._metafy(from)
try {
this._metaTyped.addConversion(
{from, to: toParts[0], convert: spec.from[from]})
{from, to: toBase, convert: spec.from[from]})
} catch {
}
}
@ -438,12 +506,12 @@ export default class PocomathInstance {
})
this._invalidateDependents(':' + nextSuper)
/* Add the conversion in the metaverse if need be: */
const toParts = nextSuper.split('<', 2)
if (toParts.length > 1 && base !== toParts[0]) {
const [toBase, toInstance] = splitTemplate(nextSuper)
if (toInstance && base !== toBase) {
this._metafy(type)
this._metaTyped.addConversion({
from: type,
to: toParts[0],
to: toBase,
convert: this.Types[to].from[fromtype]
})
}
@ -559,7 +627,7 @@ export default class PocomathInstance {
/* Used internally to install a template type */
_installTemplateType(type, spec) {
const base = type.split('<')[0]
const [base] = splitTemplate(type)
/* For now, just allow a single template per base type; that
* might need to change later:
*/
@ -760,7 +828,7 @@ export default class PocomathInstance {
let keep = true
for (const type of typesOfSignature(entry[0])) {
if (type in this.Types) continue
const baseType = type.split('<')[0]
const [baseType] = splitTemplate(type)
if (baseType in this.Templates) continue
keep = false
break
@ -857,7 +925,7 @@ export default class PocomathInstance {
othertype, theTemplateParam, '')
let otherTypeCollection = [othertype]
if (testType !== othertype) {
const base = othertype.split('<',1)[0]
const [base] = splitTemplate(othertype)
otherTypeCollection = this._instantiationsOf[base]
}
for (const possibility of otherTypeCollection) {
@ -873,43 +941,7 @@ export default class PocomathInstance {
}
for (const instType of instantiationSet) {
if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue
if (instType in behavior.hasInstantiations) continue
const signature =
substituteInSignature(rawSignature, theTemplateParam, instType)
/* Don't override an explicit implementation: */
if (signature in imps) continue
/* Don't go too deep */
let maxdepth = 0
for (const argType in typeListOfSignature(signature)) {
const depth = argType.split('<').length
if (depth > maxdepth) maxdepth = depth
}
if (maxdepth > this._maxDepthSeen + 1) continue
/* All right, go ahead and instantiate */
const uses = new Set()
for (const dep of behavior.uses) {
if (this._templateParam(dep)) continue
uses.add(substituteInSignature(dep, theTemplateParam, instType))
}
const patch = (refs) => {
const innerRefs = {}
for (const dep of behavior.uses) {
if (this._templateParam(dep)) {
innerRefs[dep] = instType
} else {
const outerName = substituteInSignature(
dep, theTemplateParam, instType)
innerRefs[dep] = refs[outerName]
}
}
return behavior.does(innerRefs)
}
this._addTFimplementation(tf_imps, signature, {uses, does: patch})
tf_imps[signature]._pocoSignature = rawSignature
tf_imps[signature]._pocoInstance = instType
behavior.hasInstantiations[instType] = signature
this._instantiateTemplateImplementation(name, rawSignature, instType)
}
/* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */
@ -1031,7 +1063,7 @@ export default class PocomathInstance {
for (const possibility of returnType.split('|')) {
const instantiated = self._maybeInstantiate(possibility)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
const [tempBase] = splitTemplate(instantiated)
self._invalidateDependents(':' + tempBase)
}
}
@ -1047,6 +1079,7 @@ export default class PocomathInstance {
}
Object.defineProperty(
patchFunc, 'name', {value: `${name}(${signature})`})
patchFunc._pocoSignature = rawSignature
return patchFunc
}
Object.defineProperty(
@ -1083,12 +1116,12 @@ export default class PocomathInstance {
let tf
if (Object.keys(tf_imps).length > 0) {
tf = this._typed(name, tf_imps)
Object.defineProperty(tf, 'fromInstance', {value: this})
tf.fromInstance = this
}
let metaTF
if (Object.keys(meta_imps).length > 0) {
metaTF = this._metaTyped(name, meta_imps)
Object.defineProperty(metaTF, 'fromInstance', {value: this})
metaTF.fromInstance = this
}
this._meta[name] = metaTF
@ -1137,11 +1170,58 @@ export default class PocomathInstance {
return undefined // no such type
}
// it's a template type, turn it into a template and an arg
let base = type.split('<',1)[0]
const arg = type.slice(base.length+1, -1)
let [base, arg] = splitTemplate(type)
return this.instantiateTemplate(base, arg)
}
/* Generate and include a template instantiation for operation name
* for the template signature templateSignature instantiated for
* instanceType, returning the resulting implementation.
*/
_instantiateTemplateImplementation(name, templateSignature, instanceType) {
if (!(instanceType in this.Types)) return undefined
if (this.Types[instanceType] === anySpec) return undefined
const imps = this._imps[name]
const behavior = imps[templateSignature]
if (instanceType in behavior.hasInstantiations) return undefined
const signature = substituteInSignature(
templateSignature, theTemplateParam, instanceType)
/* Don't override an explicit implementation: */
if (signature in imps) return undefined
/* Don't go too deep */
let maxdepth = 0
for (const argType in typeListOfSignature(signature)) {
const depth = argType.split('<').length
if (depth > maxdepth) maxdepth = depth
}
if (maxdepth > this._maxDepthSeen + 1) return undefined
/* All right, go ahead and instantiate */
const uses = new Set()
for (const dep of behavior.uses) {
if (this._templateParam(dep)) continue
uses.add(substituteInSignature(dep, theTemplateParam, instanceType))
}
const patch = (refs) => {
const innerRefs = {}
for (const dep of behavior.uses) {
if (this._templateParam(dep)) {
innerRefs[dep] = instanceType
} else {
const outerName = substituteInSignature(
dep, theTemplateParam, instanceType)
innerRefs[dep] = refs[outerName]
}
}
return behavior.does(innerRefs)
}
const tf_imps = this._TFimps[name]
this._addTFimplementation(tf_imps, signature, {uses, does: patch})
tf_imps[signature]._pocoSignature = templateSignature
tf_imps[signature]._pocoInstance = instanceType
behavior.hasInstantiations[instanceType] = signature
return tf_imps[signature]
}
/* Adapts Pocomath-style behavior specification (uses, does) for signature
* to typed-function implementations and inserts the result into plain
* object imps
@ -1254,6 +1334,7 @@ export default class PocomathInstance {
refs.self = self
const implementation = does(refs)
Object.defineProperty(implementation, 'name', {value: does.name})
implementation.fromInstance = this
// What are we going to do with the return type info in here?
return implementation
})
@ -1271,11 +1352,13 @@ export default class PocomathInstance {
deferred: true,
builtRefs: refs,
sigDoes: does,
fromInstance: this,
psr: part_self_references
}
return
}
const implementation = does(refs)
implementation.fromInstance = this
// could do something with return type information here?
imps[signature] = implementation
}
@ -1310,7 +1393,22 @@ export default class PocomathInstance {
name, this._imps[name], neededSig)
if (foundSig) {
const match = this._pocoFindSignature(name, neededSig)
refs[`self(${neededSig})`] = match.implementation
const neededTemplate = match.fn._pocoSignature
const neededInstance = whichSigInstance(
neededSig, neededTemplate)
const neededImplementation =
this._instantiateTemplateImplementation(
name, neededTemplate, neededInstance)
if (!neededImplementation) {
refs[`self(${neededSig})`] = match.implementation
} else {
if (typeof neededImplementation === 'function') {
refs[`self(${neededSig})`] = neededImplementation
} else {
corrected_self_references.push(neededSig)
remaining_self_references.push(neededSig)
}
}
} else {
throw new Error(
'Implement inexact self-reference in typed-function for '
@ -1335,6 +1433,7 @@ export default class PocomathInstance {
}
imps[aSignature]._pocoSignature = deferral._pocoSignature
imps[aSignature]._pocoInstance = deferral._pocoInstance
imps[aSignature].fromInstance = deferral.fromInstance
}
}
@ -1343,8 +1442,7 @@ export default class PocomathInstance {
* in the instance.
*/
_ensureTemplateTypes(template, type) {
const base = template.split('<', 1)[0]
const arg = template.slice(base.length + 1, -1)
const [base, arg] = splitTemplate(template)
if (!arg) {
throw new Error(
'Implementation error in _ensureTemplateTypes', template, type)
@ -1481,7 +1579,7 @@ export default class PocomathInstance {
if (otherType === 'any') continue
if (myType === otherType) continue
if (otherType in this.Templates) {
const myBase = myType.split('<',1)[0]
const [myBase] = splitTemplate(myType)
if (myBase === otherType) continue
if (this.instantiateTemplate(otherType, myType)) {
let dummy
@ -1525,10 +1623,24 @@ export default class PocomathInstance {
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
const implIndex = Math.min(i, implTypes.length - 1)
let implType = implTypes[implIndex]
if (implIndex < i) {
if (implType.slice(0,3) !== '...') {
// ran out of arguments in impl
allMatched = false
break
}
}
if (implType.slice(0,3) === '...') {
implType = implType.slice(3)
}
const hasMatch = implType.split('|').some(
t => (wantTypes[i] === t || this.isSubtypeOf(wantTypes[i], t)))
if (!hasMatch) {
allMatched = false
break
}
}
if (allMatched) return details
}

View File

@ -1,5 +1,6 @@
/* A template type representing a homeogeneous tuple of elements */
import PocomathInstance from '../../core/PocomathInstance.mjs'
import {Returns, returnTypeOf} from '../../core/Returns.mjs'
const Tuple = new PocomathInstance('Tuple')
@ -33,50 +34,66 @@ Tuple.promoteUnary = {
'Tuple<T>': ({
'self(T)': me,
tuple
}) => t => tuple(...(t.elts.map(x => me(x)))) // NOTE: this must use
// the inner arrow function to drop additional arguments that Array.map
// supplies, as otherwise the wrong signature of `me` might be used.
}) => {
const compType = me.fromInstance.joinTypes(
returnTypeOf(me).split('|'), 'convert')
return Returns(
`Tuple<${compType}>`, t => tuple(...(t.elts.map(x => me(x)))))
}
}
Tuple.promoteBinaryUnary = {
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => (s,t) => {
let i = -1
let result = []
while (true) {
i += 1
if (i < s.elts.length) {
if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i]))
else result.push(meU(s.elts[i]))
continue
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => {
const compTypes = returnTypeOf(meB).split('|').concat(
returnTypeOf(meU).split('|'))
const compType = meU.fromInstance.joinTypes(compTypes, 'convert')
return Returns(`Tuple<${compType}>`, (s,t) => {
let i = -1
let result = []
while (true) {
i += 1
if (i < s.elts.length) {
if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i]))
else result.push(meU(s.elts[i]))
continue
}
if (i < t.elts.length) result.push(meU(t.elts[i]))
else break
}
if (i < t.elts.length) result.push(meU(t.elts[i]))
else break
}
return tuple(...result)
return tuple(...result)
})
}
}
Tuple.promoteBinary = {
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, tuple}) => (s,t) => {
const lim = Math.max(s.elts.length, t.elts.length)
const result = []
for (let i = 0; i < lim; ++i) {
result.push(meB(s.elts[i], t.elts[i]))
}
return tuple(...result)
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, tuple}) => {
const compType = meB.fromInstance.joinTypes(
returnTypeOf(meB).split('|'))
return Returns(`Tuple<${compType}>`, (s,t) => {
const lim = Math.max(s.elts.length, t.elts.length)
const result = []
for (let i = 0; i < lim; ++i) {
result.push(meB(s.elts[i], t.elts[i]))
}
return tuple(...result)
})
}
}
Tuple.promoteBinaryStrict = {
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, tuple}) => (s,t) => {
if (s.elts.length !== t.elts.length) {
throw new RangeError('Tuple length mismatch') // get name of self ??
}
const result = []
for (let i = 0; i < s.elts.length; ++i) {
result.push(meB(s.elts[i], t.elts[i]))
}
return tuple(...result)
'Tuple<T>,Tuple<T>': ({'self(T,T)': meB, tuple}) => {
const compType = meB.fromInstance.joinTypes(
returnTypeOf(meB).split('|'))
return Returns(`Tuple<${compType}>`, (s,t) => {
if (s.elts.length !== t.elts.length) {
throw new RangeError('Tuple length mismatch') // get name of self ??
}
const result = []
for (let i = 0; i < s.elts.length; ++i) {
result.push(meB(s.elts[i], t.elts[i]))
}
return tuple(...result)
})
}
}

View File

@ -1,11 +1,16 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Tuple.mjs'
export const equalTT = {
'Tuple<T>,Tuple<T>': ({'self(T,T)': me, 'length(Tuple)': len}) => (s,t) => {
'Tuple<T>,Tuple<T>': ({
'self(T,T)': me,
'length(Tuple)': len
}) => Returns('boolean', (s,t) => {
if (len(s) !== len(t)) return false
for (let i = 0; i < len(s); ++i) {
if (!me(s.elts[i], t.elts[i])) return false
}
return true
}
})
}

View File

@ -1,7 +1,10 @@
import Returns from '../core/Returns.mjs'
export {Tuple} from './Types/Tuple.mjs'
export const isZero = {
'Tuple<T>': ({'self(T)': me}) => t => t.elts.every(e => me(e))
'Tuple<T>': ({'self(T)': me}) => Returns(
'boolean', 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

@ -1,6 +1,10 @@
import Returns from '../core/Returns.mjs'
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 = {'...T': () => args => ({elts: args})}
export const tuple = {
'...T': ({T}) => Returns(`Tuple<${T}>`, args => ({elts: args}))
}

View File

@ -54,6 +54,9 @@ describe('tuple', () => {
assert.deepStrictEqual(
math.subtract(math.tuple(3n,4n,5n), math.tuple(2n,1n,0n)),
math.tuple(1n,3n,5n))
assert.deepStrictEqual(
math.returnTypeOf('subtract', 'Tuple<bigint>,Tuple<bigint>'),
'Tuple<bigint>')
assert.throws(
() => math.subtract(math.tuple(5,6), math.tuple(7)),
/RangeError/)
@ -104,9 +107,16 @@ describe('tuple', () => {
})
it('supports sqrt', () => {
const mixedTuple = math.tuple(2, math.complex(0,2), 1.5)
assert.deepStrictEqual(
math.sqrt(math.tuple(4,-4,2.25)),
math.tuple(2, math.complex(0,2), 1.5))
mixedTuple,
math.tuple(math.complex(2), math.complex(0,2), math.complex(1.5)))
assert.strictEqual(
math.returnTypeOf('tuple', 'NumInt, Complex<NumInt>, number'),
'Tuple<Complex<number>>')
assert.deepStrictEqual(math.sqrt(math.tuple(4,-4,2.25)), mixedTuple)
assert.strictEqual(
math.returnTypeOf('sqrt', 'Tuple<NumInt>'), 'Tuple<Complex<number>>')
})
})