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

View File

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

View File

@ -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.
*/

View File

@ -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', () => {