feat: Return type annotations #53

Merged
glen merged 15 commits from return_types into main 2022-08-30 19:36:44 +00:00
4 changed files with 77 additions and 65 deletions
Showing only changes of commit f7bb3697ed - Show all commits

View File

@ -2,7 +2,7 @@
import typed from 'typed-function' import typed from 'typed-function'
import {makeChain} from './Chain.mjs' import {makeChain} from './Chain.mjs'
import {dependencyExtractor, generateTypeExtractor} from './extractors.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' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type 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 /* Returns a new signature just like sig but with the parameter replaced by
* the type * 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') const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
return sig.replaceAll(pattern, type) return sig.replaceAll(pattern, type)
} }
@ -30,12 +32,14 @@ export default class PocomathInstance {
'install', 'install',
'installType', 'installType',
'instantiateTemplate', 'instantiateTemplate',
'isPriorTo',
'isSubtypeOf', 'isSubtypeOf',
'joinTypes', 'joinTypes',
'name', 'name',
'returnTypeOf', 'returnTypeOf',
'self', 'self',
'subtypesOf', 'subtypesOf',
'supertypesOf',
'Templates', 'Templates',
'typeOf', 'typeOf',
'Types', 'Types',
@ -88,7 +92,7 @@ export default class PocomathInstance {
this._installFunctions({ this._installFunctions({
typeOf: { 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' if (!(this._typed.isTypedFunction(operation))) return 'any'
const details = this._typed.findSignature(operation, signature) 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: */ /* Return a chain object for this instance with a given value: */
@ -398,7 +402,7 @@ export default class PocomathInstance {
} }
// update the typeOf function // update the typeOf function
const imp = {} const imp = {}
imp[type] = {uses: new Set(), does: () => R_('string', () => type)} imp[type] = {uses: new Set(), does: () => Returns('string', () => type)}
this._installFunctions({typeOf: imp}) this._installFunctions({typeOf: imp})
} }
@ -412,18 +416,36 @@ export default class PocomathInstance {
supSubs.splice(i, 0, sub) 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) { isSubtypeOf(typeA, typeB) {
return this._subtypes[typeB].includes(typeA) 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 /* 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). * order (i.e, no type on the list contains one that comes after it).
*/ */
subtypesOf(type) { subtypesOf(type) {
// HERE! For this to work, have to maintain subtypes as a sorted list.
return this._subtypes[type] // should we clone? 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, /* Returns the most refined type containing all the types in the array,
* with '' standing for the empty type for convenience. If the second * with '' standing for the empty type for convenience. If the second
* argument `convert` is true, a convertible type is considered a * argument `convert` is true, a convertible type is considered a
@ -488,7 +510,7 @@ export default class PocomathInstance {
const imp = {} const imp = {}
imp[type] = { imp[type] = {
uses: new Set(['T']), uses: new Set(['T']),
does: ({T}) => R_('string', () => `${base}<${T}>`) does: ({T}) => Returns('string', () => `${base}<${T}>`)
} }
this._installFunctions({typeOf: imp}) this._installFunctions({typeOf: imp})
@ -628,7 +650,7 @@ export default class PocomathInstance {
/* Check if it's an ordinary non-template signature */ /* Check if it's an ordinary non-template signature */
let explicit = true let explicit = true
for (const type of typesOfSignature(rawSignature)) { for (const type of typesOfSignature(rawSignature)) {
for (const word of type.split(/[<>]/)) { for (const word of type.split(/[<>(=\s]/)) {
if (this._templateParam(word)) { if (this._templateParam(word)) {
explicit = false explicit = false
break break
@ -640,14 +662,26 @@ export default class PocomathInstance {
continue continue
} }
/* It's a template, have to instantiate */ /* 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 */ /* First, add the known instantiations, gathering all types needed */
if (!('instantiations' in behavior)) { if (!('instantiations' in behavior)) {
behavior.instantiations = new Set() behavior.instantiations = new Set()
if (ubType) behavior.instantiations.add(ubType)
} }
let instantiationSet = new Set() let instantiationSet = new Set()
for (const instType of behavior.instantiations) { for (const instType of behavior.instantiations) {
instantiationSet.add(instType) instantiationSet.add(instType)
for (const other of this._priorTypes[instType]) { for (const other of this.subtypesOf(instType)) {
instantiationSet.add(other) instantiationSet.add(other)
} }
} }
@ -656,7 +690,7 @@ export default class PocomathInstance {
if (!(instType in this.Types)) continue if (!(instType in this.Types)) continue
if (this.Types[instType] === anySpec) continue if (this.Types[instType] === anySpec) continue
const signature = const signature =
substituteInSig(rawSignature, theTemplateParam, instType) substituteInSignature(rawSignature, theTemplateParam, instType)
/* Don't override an explicit implementation: */ /* Don't override an explicit implementation: */
if (signature in imps) continue if (signature in imps) continue
/* Don't go too deep */ /* Don't go too deep */
@ -670,7 +704,7 @@ export default class PocomathInstance {
const uses = new Set() const uses = new Set()
for (const dep of behavior.uses) { for (const dep of behavior.uses) {
if (this._templateParam(dep)) continue if (this._templateParam(dep)) continue
uses.add(substituteInSig(dep, theTemplateParam, instType)) uses.add(substituteInSignature(dep, theTemplateParam, instType))
} }
const patch = (refs) => { const patch = (refs) => {
const innerRefs = {} const innerRefs = {}
@ -678,7 +712,7 @@ export default class PocomathInstance {
if (this._templateParam(dep)) { if (this._templateParam(dep)) {
innerRefs[dep] = instType innerRefs[dep] = instType
} else { } else {
const outerName = substituteInSig( const outerName = substituteInSignature(
dep, theTemplateParam, instType) dep, theTemplateParam, instType)
innerRefs[dep] = refs[outerName] innerRefs[dep] = refs[outerName]
} }
@ -689,11 +723,13 @@ export default class PocomathInstance {
tf_imps, signature, {uses, does: patch}) tf_imps, signature, {uses, does: patch})
} }
/* Now add the catchall signature */ /* Now add the catchall signature */
/* (Not needed if if it's a bounded template) */
if (ubType) continue
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 = rawSignature.replaceAll(templateCall, '') let baseSignature = rawSignature.replaceAll(templateCall, '')
/* Any remaining template params are top-level */ /* Any remaining template params are top-level */
const signature = substituteInSig( const signature = substituteInSignature(
baseSignature, theTemplateParam, 'ground') baseSignature, theTemplateParam, 'ground')
/* The catchall signature has to detect the actual type of the call /* The catchall signature has to detect the actual type of the call
* and add the new instantiations. * and add the new instantiations.
@ -718,7 +754,8 @@ export default class PocomathInstance {
for (const dep of behavior.uses) { for (const dep of behavior.uses) {
let [func, needsig] = dep.split(/[()]/) let [func, needsig] = dep.split(/[()]/)
if (needsig) { if (needsig) {
const subsig = substituteInSig(needsig, theTemplateParam, '') const subsig = substituteInSignature(
needsig, theTemplateParam, '')
if (subsig === needsig) { if (subsig === needsig) {
simplifiedUses[dep] = dep simplifiedUses[dep] = dep
} else { } else {
@ -789,7 +826,7 @@ export default class PocomathInstance {
self._maxDepthSeen = depth self._maxDepthSeen = depth
} }
/* Generate the list of actual wanted types */ /* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSig( const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor)) type, theTemplateParam, instantiateFor))
/* Now we have to add any actual types that are relevant /* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal parameter * to this invocation. Namely, that would be every formal parameter
@ -811,7 +848,7 @@ export default class PocomathInstance {
if (wantType.slice(0,3) === '...') { if (wantType.slice(0,3) === '...') {
wantType = wantType.slice(3) wantType = wantType.slice(3)
} }
wantType = substituteInSig( wantType = substituteInSignature(
wantType, theTemplateParam, instantiateFor) 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)
@ -840,7 +877,7 @@ export default class PocomathInstance {
} else { } else {
let [func, needsig] = dep.split(/[()]/) let [func, needsig] = dep.split(/[()]/)
if (self._typed.isTypedFunction(refs[simplifiedDep])) { if (self._typed.isTypedFunction(refs[simplifiedDep])) {
const subsig = substituteInSig( const subsig = substituteInSignature(
needsig, theTemplateParam, instantiateFor) needsig, theTemplateParam, instantiateFor)
let resname = simplifiedDep let resname = simplifiedDep
if (resname == 'self') resname = name if (resname == 'self') resname = name
@ -890,8 +927,8 @@ export default class PocomathInstance {
} }
/* Adapts Pocomath-style behavior specification (uses, does) for signature /* Adapts Pocomath-style behavior specification (uses, does) for signature
* to typed-function implementations and inserts the result into plain object * to typed-function implementations and inserts the result into plain
* imps * object imps
*/ */
_addTFimplementation(imps, signature, behavior) { _addTFimplementation(imps, signature, behavior) {
const {uses, does} = behavior const {uses, does} = behavior
@ -910,7 +947,7 @@ export default class PocomathInstance {
* Verify that the desired signature has been fully grounded: * Verify that the desired signature has been fully grounded:
*/ */
if (needsig) { if (needsig) {
const trysig = substituteInSig(needsig, theTemplateParam, '') const trysig = substituteInSignature(needsig, theTemplateParam, '')
if (trysig !== needsig) { if (trysig !== needsig) {
throw new Error( throw new Error(
'Attempt to add a template implementation: ' + 'Attempt to add a template implementation: ' +
@ -1051,8 +1088,9 @@ export default class PocomathInstance {
return resultingTypes return resultingTypes
} }
/* Maybe add the instantiation of template type base with argument tyoe /* Maybe add the instantiation of template type base with argument type
* instantiator to the Types of this instance, if it hasn't happened already. * 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, * 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). * and undefined if the type is declined (because of being nested too deep).
*/ */
@ -1081,7 +1119,7 @@ export default class PocomathInstance {
if (template.before) { if (template.before) {
for (const beforeTmpl of template.before) { for (const beforeTmpl of template.before) {
beforeTypes.push( beforeTypes.push(
substituteInSig(beforeTmpl, theTemplateParam, instantiator)) substituteInSignature(beforeTmpl, theTemplateParam, instantiator))
} }
} }
if (beforeTypes.length > 0) { if (beforeTypes.length > 0) {
@ -1090,7 +1128,7 @@ export default class PocomathInstance {
newTypeSpec.test = template.test(this._typeTests[instantiator]) newTypeSpec.test = template.test(this._typeTests[instantiator])
if (template.from) { if (template.from) {
for (let source in template.from) { for (let source in template.from) {
const instSource = substituteInSig( const instSource = substituteInSignature(
source, theTemplateParam, instantiator) source, theTemplateParam, instantiator)
let usesFromParam = false let usesFromParam = false
for (const word of instSource.split(/[<>]/)) { for (const word of instSource.split(/[<>]/)) {
@ -1101,14 +1139,14 @@ 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 = substituteInSignature(
instSource, templateFromParam, iFrom) instSource, templateFromParam, iFrom)
maybeFrom[finalSource] = template.from[source]( maybeFrom[finalSource] = template.from[source](
instantiatorSpec.from[iFrom]) instantiatorSpec.from[iFrom])
} }
// Assuming all templates are covariant here, I guess... // Assuming all templates are covariant here, I guess...
for (const subType of this._subtypes[instantiator]) { for (const subType of this._subtypes[instantiator]) {
const finalSource = substituteInSig( const finalSource = substituteInSignature(
instSource, templateFromParam, subType) instSource, templateFromParam, subType)
maybeFrom[finalSource] = template.from[source](x => x) maybeFrom[finalSource] = template.from[source](x => x)
} }

View File

@ -15,15 +15,20 @@ function cloneFunction(fn) {
return theClone return theClone
} }
export function R_(type, fn) { export function Returns(type, fn) {
if ('returns' in fn) fn = cloneFunction(fn) if ('returns' in fn) fn = cloneFunction(fn)
fn.returns = type fn.returns = type
return fn return fn
} }
export function returnTypeOf(fn) { export function returnTypeOf(fn, signature, pmInstance) {
return fn.returns || 'any' 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

View File

@ -1,37 +1,6 @@
import R_ from '../core/returns.mjs' import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const negate = { export const negate = {
NumInt: () => R_('NumInt', n => -n), 'T (= number': ({T}) => Returns(T, n => -n)
number: () => R_('number', 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.
*/

View File

@ -27,7 +27,7 @@ describe('The default full pocomath instance "math"', () => {
it('can subtract numbers', () => { it('can subtract numbers', () => {
assert.strictEqual(math.subtract(12, 5), 7) 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', () => { it('can add numbers', () => {