feat(PocomathInstance): Add bounded template parameters
This feature helps specify the return type of implementations where the return type depends on the exact subtype that the implementation was called with, such as negate.
This commit is contained in:
parent
dc6921e768
commit
f7bb3697ed
@ -2,7 +2,7 @@
|
||||
import typed from 'typed-function'
|
||||
import {makeChain} from './Chain.mjs'
|
||||
import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs'
|
||||
import {R_, returnTypeOf} from './returns.mjs'
|
||||
import {Returns, returnTypeOf} from './Returns.mjs'
|
||||
import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs'
|
||||
|
||||
const anySpec = {} // fixed dummy specification of 'any' type
|
||||
@ -13,7 +13,9 @@ const templateFromParam = 'U' // For defining covariant conversions
|
||||
/* Returns a new signature just like sig but with the parameter replaced by
|
||||
* the type
|
||||
*/
|
||||
function substituteInSig(sig, parameter, type) {
|
||||
const upperBounds = /\s*[(]=\s*(\w*)\s*/g
|
||||
function substituteInSignature(signature, parameter, type) {
|
||||
const sig = signature.replaceAll(upperBounds, '')
|
||||
const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
|
||||
return sig.replaceAll(pattern, type)
|
||||
}
|
||||
@ -30,12 +32,14 @@ export default class PocomathInstance {
|
||||
'install',
|
||||
'installType',
|
||||
'instantiateTemplate',
|
||||
'isPriorTo',
|
||||
'isSubtypeOf',
|
||||
'joinTypes',
|
||||
'name',
|
||||
'returnTypeOf',
|
||||
'self',
|
||||
'subtypesOf',
|
||||
'supertypesOf',
|
||||
'Templates',
|
||||
'typeOf',
|
||||
'Types',
|
||||
@ -88,7 +92,7 @@ export default class PocomathInstance {
|
||||
|
||||
this._installFunctions({
|
||||
typeOf: {
|
||||
ground: {uses: new Set(), does: () => R_('string', () => 'any')}
|
||||
ground: {uses: new Set(), does: () => Returns('string', () => 'any')}
|
||||
}
|
||||
})
|
||||
|
||||
@ -202,7 +206,7 @@ export default class PocomathInstance {
|
||||
}
|
||||
if (!(this._typed.isTypedFunction(operation))) return 'any'
|
||||
const details = this._typed.findSignature(operation, signature)
|
||||
return returnTypeOf(details.fn)
|
||||
return returnTypeOf(details.fn, signature, this)
|
||||
}
|
||||
|
||||
/* Return a chain object for this instance with a given value: */
|
||||
@ -398,7 +402,7 @@ export default class PocomathInstance {
|
||||
}
|
||||
// update the typeOf function
|
||||
const imp = {}
|
||||
imp[type] = {uses: new Set(), does: () => R_('string', () => type)}
|
||||
imp[type] = {uses: new Set(), does: () => Returns('string', () => type)}
|
||||
this._installFunctions({typeOf: imp})
|
||||
}
|
||||
|
||||
@ -412,18 +416,36 @@ export default class PocomathInstance {
|
||||
supSubs.splice(i, 0, sub)
|
||||
}
|
||||
|
||||
/* Returns true is typeA is a subtype of type B */
|
||||
/* Returns true if typeA is a subtype of type B */
|
||||
isSubtypeOf(typeA, typeB) {
|
||||
return this._subtypes[typeB].includes(typeA)
|
||||
}
|
||||
|
||||
/* Returns true if typeA is a subtype of or converts to type B */
|
||||
isPriorTo(typeA, typeB) {
|
||||
if (!(typeB in this._priorTypes)) return false
|
||||
return this._priorTypes[typeB].has(typeA)
|
||||
}
|
||||
|
||||
/* Returns a list of the subtypes of a given type, in topological sorted
|
||||
* order (i.e, no type on the list contains one that comes after it).
|
||||
*/
|
||||
subtypesOf(type) {
|
||||
// HERE! For this to work, have to maintain subtypes as a sorted list.
|
||||
return this._subtypes[type] // should we clone?
|
||||
}
|
||||
|
||||
/* Returns a list of the supertypes of a given type, starting with itself,
|
||||
* in topological order
|
||||
*/
|
||||
supertypesOf(type) {
|
||||
const supList = []
|
||||
while (type) {
|
||||
supList.push(type)
|
||||
type = this.Types[type].refines
|
||||
}
|
||||
return supList
|
||||
}
|
||||
|
||||
/* Returns the most refined type containing all the types in the array,
|
||||
* with '' standing for the empty type for convenience. If the second
|
||||
* argument `convert` is true, a convertible type is considered a
|
||||
@ -488,7 +510,7 @@ export default class PocomathInstance {
|
||||
const imp = {}
|
||||
imp[type] = {
|
||||
uses: new Set(['T']),
|
||||
does: ({T}) => R_('string', () => `${base}<${T}>`)
|
||||
does: ({T}) => Returns('string', () => `${base}<${T}>`)
|
||||
}
|
||||
this._installFunctions({typeOf: imp})
|
||||
|
||||
@ -628,7 +650,7 @@ export default class PocomathInstance {
|
||||
/* Check if it's an ordinary non-template signature */
|
||||
let explicit = true
|
||||
for (const type of typesOfSignature(rawSignature)) {
|
||||
for (const word of type.split(/[<>]/)) {
|
||||
for (const word of type.split(/[<>(=\s]/)) {
|
||||
if (this._templateParam(word)) {
|
||||
explicit = false
|
||||
break
|
||||
@ -640,14 +662,26 @@ export default class PocomathInstance {
|
||||
continue
|
||||
}
|
||||
/* It's a template, have to instantiate */
|
||||
/* First, find any upper bounds on the instantation */
|
||||
/* TODO: handle multiple upper bounds */
|
||||
upperBounds.lastIndex = 0
|
||||
let ubType = upperBounds.exec(rawSignature)
|
||||
if (ubType) {
|
||||
ubType = ubType[1]
|
||||
if (!ubType in this.Types) {
|
||||
throw new TypeError(
|
||||
`Unknown type upper bound '${ubType}' in '${rawSignature}'`)
|
||||
}
|
||||
}
|
||||
/* First, add the known instantiations, gathering all types needed */
|
||||
if (!('instantiations' in behavior)) {
|
||||
behavior.instantiations = new Set()
|
||||
if (ubType) behavior.instantiations.add(ubType)
|
||||
}
|
||||
let instantiationSet = new Set()
|
||||
for (const instType of behavior.instantiations) {
|
||||
instantiationSet.add(instType)
|
||||
for (const other of this._priorTypes[instType]) {
|
||||
for (const other of this.subtypesOf(instType)) {
|
||||
instantiationSet.add(other)
|
||||
}
|
||||
}
|
||||
@ -656,7 +690,7 @@ export default class PocomathInstance {
|
||||
if (!(instType in this.Types)) continue
|
||||
if (this.Types[instType] === anySpec) continue
|
||||
const signature =
|
||||
substituteInSig(rawSignature, theTemplateParam, instType)
|
||||
substituteInSignature(rawSignature, theTemplateParam, instType)
|
||||
/* Don't override an explicit implementation: */
|
||||
if (signature in imps) continue
|
||||
/* Don't go too deep */
|
||||
@ -670,7 +704,7 @@ export default class PocomathInstance {
|
||||
const uses = new Set()
|
||||
for (const dep of behavior.uses) {
|
||||
if (this._templateParam(dep)) continue
|
||||
uses.add(substituteInSig(dep, theTemplateParam, instType))
|
||||
uses.add(substituteInSignature(dep, theTemplateParam, instType))
|
||||
}
|
||||
const patch = (refs) => {
|
||||
const innerRefs = {}
|
||||
@ -678,7 +712,7 @@ export default class PocomathInstance {
|
||||
if (this._templateParam(dep)) {
|
||||
innerRefs[dep] = instType
|
||||
} else {
|
||||
const outerName = substituteInSig(
|
||||
const outerName = substituteInSignature(
|
||||
dep, theTemplateParam, instType)
|
||||
innerRefs[dep] = refs[outerName]
|
||||
}
|
||||
@ -689,11 +723,13 @@ export default class PocomathInstance {
|
||||
tf_imps, signature, {uses, does: patch})
|
||||
}
|
||||
/* Now add the catchall signature */
|
||||
/* (Not needed if if it's a bounded template) */
|
||||
if (ubType) continue
|
||||
let templateCall = `<${theTemplateParam}>`
|
||||
/* Relying here that the base of 'Foo<T>' is 'Foo': */
|
||||
let baseSignature = rawSignature.replaceAll(templateCall, '')
|
||||
/* Any remaining template params are top-level */
|
||||
const signature = substituteInSig(
|
||||
const signature = substituteInSignature(
|
||||
baseSignature, theTemplateParam, 'ground')
|
||||
/* The catchall signature has to detect the actual type of the call
|
||||
* and add the new instantiations.
|
||||
@ -718,7 +754,8 @@ export default class PocomathInstance {
|
||||
for (const dep of behavior.uses) {
|
||||
let [func, needsig] = dep.split(/[()]/)
|
||||
if (needsig) {
|
||||
const subsig = substituteInSig(needsig, theTemplateParam, '')
|
||||
const subsig = substituteInSignature(
|
||||
needsig, theTemplateParam, '')
|
||||
if (subsig === needsig) {
|
||||
simplifiedUses[dep] = dep
|
||||
} else {
|
||||
@ -789,7 +826,7 @@ export default class PocomathInstance {
|
||||
self._maxDepthSeen = depth
|
||||
}
|
||||
/* Generate the list of actual wanted types */
|
||||
const wantTypes = parTypes.map(type => substituteInSig(
|
||||
const wantTypes = parTypes.map(type => substituteInSignature(
|
||||
type, theTemplateParam, instantiateFor))
|
||||
/* Now we have to add any actual types that are relevant
|
||||
* to this invocation. Namely, that would be every formal parameter
|
||||
@ -811,7 +848,7 @@ export default class PocomathInstance {
|
||||
if (wantType.slice(0,3) === '...') {
|
||||
wantType = wantType.slice(3)
|
||||
}
|
||||
wantType = substituteInSig(
|
||||
wantType = substituteInSignature(
|
||||
wantType, theTemplateParam, instantiateFor)
|
||||
if (wantType !== parTypes[i]) {
|
||||
args[j] = self._typed.convert(args[j], wantType)
|
||||
@ -840,7 +877,7 @@ export default class PocomathInstance {
|
||||
} else {
|
||||
let [func, needsig] = dep.split(/[()]/)
|
||||
if (self._typed.isTypedFunction(refs[simplifiedDep])) {
|
||||
const subsig = substituteInSig(
|
||||
const subsig = substituteInSignature(
|
||||
needsig, theTemplateParam, instantiateFor)
|
||||
let resname = simplifiedDep
|
||||
if (resname == 'self') resname = name
|
||||
@ -890,8 +927,8 @@ export default class PocomathInstance {
|
||||
}
|
||||
|
||||
/* Adapts Pocomath-style behavior specification (uses, does) for signature
|
||||
* to typed-function implementations and inserts the result into plain object
|
||||
* imps
|
||||
* to typed-function implementations and inserts the result into plain
|
||||
* object imps
|
||||
*/
|
||||
_addTFimplementation(imps, signature, behavior) {
|
||||
const {uses, does} = behavior
|
||||
@ -910,7 +947,7 @@ export default class PocomathInstance {
|
||||
* Verify that the desired signature has been fully grounded:
|
||||
*/
|
||||
if (needsig) {
|
||||
const trysig = substituteInSig(needsig, theTemplateParam, '')
|
||||
const trysig = substituteInSignature(needsig, theTemplateParam, '')
|
||||
if (trysig !== needsig) {
|
||||
throw new Error(
|
||||
'Attempt to add a template implementation: ' +
|
||||
@ -1051,8 +1088,9 @@ export default class PocomathInstance {
|
||||
return resultingTypes
|
||||
}
|
||||
|
||||
/* Maybe add the instantiation of template type base with argument tyoe
|
||||
* instantiator to the Types of this instance, if it hasn't happened already.
|
||||
/* Maybe add the instantiation of template type base with argument type
|
||||
* instantiator to the Types of this instance, if it hasn't happened
|
||||
* already.
|
||||
* Returns the name of the type if added, false if it was already there,
|
||||
* and undefined if the type is declined (because of being nested too deep).
|
||||
*/
|
||||
@ -1081,7 +1119,7 @@ export default class PocomathInstance {
|
||||
if (template.before) {
|
||||
for (const beforeTmpl of template.before) {
|
||||
beforeTypes.push(
|
||||
substituteInSig(beforeTmpl, theTemplateParam, instantiator))
|
||||
substituteInSignature(beforeTmpl, theTemplateParam, instantiator))
|
||||
}
|
||||
}
|
||||
if (beforeTypes.length > 0) {
|
||||
@ -1090,7 +1128,7 @@ export default class PocomathInstance {
|
||||
newTypeSpec.test = template.test(this._typeTests[instantiator])
|
||||
if (template.from) {
|
||||
for (let source in template.from) {
|
||||
const instSource = substituteInSig(
|
||||
const instSource = substituteInSignature(
|
||||
source, theTemplateParam, instantiator)
|
||||
let usesFromParam = false
|
||||
for (const word of instSource.split(/[<>]/)) {
|
||||
@ -1101,14 +1139,14 @@ export default class PocomathInstance {
|
||||
}
|
||||
if (usesFromParam) {
|
||||
for (const iFrom in instantiatorSpec.from) {
|
||||
const finalSource = substituteInSig(
|
||||
const finalSource = substituteInSignature(
|
||||
instSource, templateFromParam, iFrom)
|
||||
maybeFrom[finalSource] = template.from[source](
|
||||
instantiatorSpec.from[iFrom])
|
||||
}
|
||||
// Assuming all templates are covariant here, I guess...
|
||||
for (const subType of this._subtypes[instantiator]) {
|
||||
const finalSource = substituteInSig(
|
||||
const finalSource = substituteInSignature(
|
||||
instSource, templateFromParam, subType)
|
||||
maybeFrom[finalSource] = template.from[source](x => x)
|
||||
}
|
||||
|
@ -15,15 +15,20 @@ function cloneFunction(fn) {
|
||||
return theClone
|
||||
}
|
||||
|
||||
export function R_(type, fn) {
|
||||
export function Returns(type, fn) {
|
||||
if ('returns' in fn) fn = cloneFunction(fn)
|
||||
fn.returns = type
|
||||
return fn
|
||||
}
|
||||
|
||||
export function returnTypeOf(fn) {
|
||||
return fn.returns || 'any'
|
||||
export function returnTypeOf(fn, signature, pmInstance) {
|
||||
const typeOfReturns = typeof fn.returns
|
||||
if (typeOfReturns === 'undefined') return 'any'
|
||||
if (typeOfReturns === 'string') return fn.returns
|
||||
// not sure if we will need a function to determine the return type,
|
||||
// but allow it for now:
|
||||
return fn.returns(signature, pmInstance)
|
||||
}
|
||||
|
||||
export default R_
|
||||
export default Returns
|
||||
|
@ -1,37 +1,6 @@
|
||||
import R_ from '../core/returns.mjs'
|
||||
import Returns from '../core/Returns.mjs'
|
||||
export * from './Types/number.mjs'
|
||||
|
||||
export const negate = {
|
||||
NumInt: () => R_('NumInt', n => -n),
|
||||
number: () => R_('number', n => -n)
|
||||
'T (= number': ({T}) => Returns(T, n => -n)
|
||||
}
|
||||
/* Can we find a mechanism to avoid reiterating the definition
|
||||
for each number (sub)type? Maybe something like:
|
||||
|
||||
export const negate = {'T:number': ({T}) => R_(T, n => -n)}
|
||||
|
||||
where 'T:number' is a new notation that means "T is a (non-strict) subtype
|
||||
of number". That doesn't look too bad, but if we went down that route, we
|
||||
might also need to make sure that if we ever implemented a PositiveNumber
|
||||
type and a NegativeNumber type then we could say
|
||||
|
||||
export const negate = {
|
||||
PositiveNumber: () => R_('NegativeNumber', n => -n),
|
||||
NegativeNumber: () => R_('PositiveNumber', n => -n),
|
||||
'T:number': ({T}) => R_(T, n => -n)
|
||||
}
|
||||
|
||||
|
||||
This just gets worse if there are also PosNumInt and NegNumInt types;
|
||||
maybe Positive<T>, Negative<T>, and Zero<T> are template types, so that
|
||||
we could say:
|
||||
|
||||
export const negate = {
|
||||
'Positive<T:number>': ({'Negative<T>': negT}) => R_(negT, n => -n),
|
||||
'Negative<T:number>': ({'Positive<T>': posT}) => R_(posT, n => -n),
|
||||
T:number: ({T}) => R_(T, n => -n)
|
||||
}
|
||||
|
||||
But all of that is pretty speculative, for now let's just go with writing
|
||||
out the different types.
|
||||
*/
|
||||
|
@ -27,7 +27,7 @@ describe('The default full pocomath instance "math"', () => {
|
||||
|
||||
it('can subtract numbers', () => {
|
||||
assert.strictEqual(math.subtract(12, 5), 7)
|
||||
//assert.strictEqual(math.subtract(3n, 1.5), 1.5)
|
||||
assert.throws(() => math.subtract(3n, 1.5), 'TypeError')
|
||||
})
|
||||
|
||||
it('can add numbers', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user