feat: Return type annotations #53

Merged
glen merged 15 commits from return_types into main 2022-08-30 19:36:44 +00:00
12 changed files with 170 additions and 53 deletions
Showing only changes of commit 775bb9ddb7 - Show all commits

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/bigint.mjs' export * from './Types/bigint.mjs'
export const negate = {bigint: () => b => -b} export const negate = {bigint: () => Returns('bigint', b => -b)}

View File

@ -1,3 +1,4 @@
import {Returns, returnTypeOf} from '../../core/Returns.mjs'
import PocomathInstance from '../../core/PocomathInstance.mjs' import PocomathInstance from '../../core/PocomathInstance.mjs'
const Complex = new PocomathInstance('Complex') const Complex = new PocomathInstance('Complex')
@ -21,7 +22,12 @@ Complex.installType('Complex<T>', {
}) })
Complex.promoteUnary = { Complex.promoteUnary = {
'Complex<T>': ({'self(T)': me, complex}) => z => complex(me(z.re), me(z.im)) 'Complex<T>': ({
T,
'self(T)': me,
complex
}) => Returns(
`Complex<${returnTypeOf(me)}>`, z => complex(me(z.re), me(z.im)))
} }
export {Complex} export {Complex}

View File

@ -1,8 +1,10 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs' export * from './Types/Complex.mjs'
export const add = { export const add = {
'Complex<T>,Complex<T>': ({ 'Complex<T>,Complex<T>': ({
T,
'self(T,T)': me, 'self(T,T)': me,
'complex(T,T)': cplx 'complex(T,T)': cplx
}) => (w,z) => cplx(me(w.re, z.re), me(w.im, z.im)) }) => Returns(`Complex<${T}>`, (w,z) => cplx(me(w.re, z.re), me(w.im, z.im)))
} }

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs' export * from './Types/Complex.mjs'
export * from '../generic/Types/generic.mjs' export * from '../generic/Types/generic.mjs'
@ -6,15 +7,16 @@ export const complex = {
* have a numeric/scalar type, e.g. by implementing subtypes in * have a numeric/scalar type, e.g. by implementing subtypes in
* typed-function * typed-function
*/ */
'undefined': () => u => u, 'undefined': () => Returns('undefined', u => u),
'undefined,any': () => (u, y) => u, 'undefined,any': () => Returns('undefined', (u, y) => u),
'any,undefined': () => (x, u) => u, 'any,undefined': () => Returns('undefined', (x, u) => u),
'undefined,undefined': () => (u, v) => u, 'undefined,undefined': () => Returns('undefined', (u, v) => u),
'T,T': () => (x, y) => ({re: x, im: y}), 'T,T': ({T}) => Returns(`Complex<${T}>`, (x, y) => ({re: x, im: y})),
/* Take advantage of conversions in typed-function */ /* Take advantage of conversions in typed-function */
// 'Complex<T>': () => z => z // 'Complex<T>': () => z => z
/* But help out because without templates built in to typed-function, /* But help out because without templates built in to typed-function,
* type inference turns out to be too hard * type inference turns out to be too hard
*/ */
'T': ({'zero(T)': zr}) => x => ({re: x, im: zr(x)}) 'T': ({T, 'zero(T)': zr}) => Returns(
`Complex<${T}>`, x => ({re: x, im: zr(x)}))
} }

View File

@ -8,14 +8,15 @@ import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs'
const anySpec = {} // fixed dummy specification of 'any' type const anySpec = {} // fixed dummy specification of 'any' type
const theTemplateParam = 'T' // First pass: only allow this one exact parameter const theTemplateParam = 'T' // First pass: only allow this one exact parameter
const restTemplateParam = `...${theTemplateParam}`
const templateFromParam = 'U' // For defining covariant conversions 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
*/ */
const upperBounds = /\s*[(]=\s*(\w*)\s*/g const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g
function substituteInSignature(signature, parameter, type) { function substituteInSignature(signature, parameter, type) {
const sig = signature.replaceAll(upperBounds, '') const sig = signature.replaceAll(upperBounds, '$1')
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)
} }
@ -53,6 +54,14 @@ export default class PocomathInstance {
this._typed = typed.create() this._typed = typed.create()
this._typed.clear() this._typed.clear()
this._typed.addTypes([{name: 'ground', test: () => true}]) this._typed.addTypes([{name: 'ground', test: () => true}])
const me = this
const myTyped = this._typed
this._typed.onMismatch = (name, args, sigs) => {
if (me._invalid.has(name)) {
return me[name](...args) // rebuild implementation and try again
}
myTyped.throwMismatchError(name, args, sigs)
}
/* List of types installed in the instance. We start with just dummies /* List of types installed in the instance. We start with just dummies
* for the 'any' type and for type parameters: * for the 'any' type and for type parameters:
*/ */
@ -201,11 +210,13 @@ export default class PocomathInstance {
/* Determine the return type of an operation given an input signature */ /* Determine the return type of an operation given an input signature */
returnTypeOf(operation, signature) { returnTypeOf(operation, signature) {
if (typeof operation === 'string') { for (const type of typeListOfSignature(signature)) {
operation = this[operation] this._maybeInstantiate(type)
} }
if (!(this._typed.isTypedFunction(operation))) return 'any' if (typeof operation !== 'string') {
const details = this._typed.findSignature(operation, signature) operation = operation.name
}
const details = this._pocoFindSignature(operation, signature)
return returnTypeOf(details.fn, signature, this) return returnTypeOf(details.fn, signature, this)
} }
@ -650,7 +661,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(/[<>(=\s]/)) { for (const word of type.split(/[<>:\s]/)) {
if (this._templateParam(word)) { if (this._templateParam(word)) {
explicit = false explicit = false
break break
@ -667,7 +678,7 @@ export default class PocomathInstance {
upperBounds.lastIndex = 0 upperBounds.lastIndex = 0
let ubType = upperBounds.exec(rawSignature) let ubType = upperBounds.exec(rawSignature)
if (ubType) { if (ubType) {
ubType = ubType[1] ubType = ubType[2]
if (!ubType in this.Types) { if (!ubType in this.Types) {
throw new TypeError( throw new TypeError(
`Unknown type upper bound '${ubType}' in '${rawSignature}'`) `Unknown type upper bound '${ubType}' in '${rawSignature}'`)
@ -681,7 +692,9 @@ export default class PocomathInstance {
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.subtypesOf(instType)) { const otherTypes =
ubType ? this.subtypesOf(instType) : this._priorTypes[instType]
for (const other of otherTypes) {
instantiationSet.add(other) instantiationSet.add(other)
} }
} }
@ -828,6 +841,7 @@ export default class PocomathInstance {
/* Generate the list of actual wanted types */ /* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSignature( const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor)) type, theTemplateParam, instantiateFor))
const wantSig = wantTypes.join(',')
/* 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
* type in the invocation, with the parameter template instantiated * type in the invocation, with the parameter template instantiated
@ -890,9 +904,22 @@ export default class PocomathInstance {
} }
// Finally ready to make the call. // Finally ready to make the call.
const implementation = behavior.does(innerRefs) const implementation = behavior.does(innerRefs)
// Could do something with return type information here // We can access return type information here
// And in particular, if it's a template, we should try to
// instantiate it:
const returnType = returnTypeOf(implementation, wantSig, self)
const instantiated = self._maybeInstantiate(returnType)
if (instantiated) {
const tempBase = instantiated.split('<',1)[0]
self._invalidateDependents(':' + tempBase)
}
return implementation(...args) return implementation(...args)
} }
Object.defineProperty(patch, 'name', {value: `${name}(${signature})`})
// TODO: Decorate patch with a function that calculates the
// correct return type a priori. Deferring because unclear what
// aspects will be merged into typed-function.
//
// The actual uses value needs to be a set: // The actual uses value needs to be a set:
const outerUses = new Set(Object.values(simplifiedUses)) const outerUses = new Set(Object.values(simplifiedUses))
this._addTFimplementation( this._addTFimplementation(
@ -905,16 +932,8 @@ export default class PocomathInstance {
const badSigs = new Set() const badSigs = new Set()
for (const sig in tf_imps) { for (const sig in tf_imps) {
for (const type of typeListOfSignature(sig)) { for (const type of typeListOfSignature(sig)) {
if (type.includes('<')) { if (this._maybeInstantiate(type) === undefined) {
// it's a template type, turn it into a template and an arg badSigs.add(sig)
let base = type.split('<',1)[0]
const arg = type.slice(base.length+1, -1)
if (base.slice(0,3) === '...') {
base = base.slice(3)
}
if (this.instantiateTemplate(base, arg) === undefined) {
badSigs.add(sig)
}
} }
} }
} }
@ -926,6 +945,29 @@ export default class PocomathInstance {
return tf return tf
} }
/* Takes an arbitrary type and performs an instantiation if necessary.
* @param {string} type The type to instantiate
* @param {string | bool | undefined }
* Returns the name of the type if an instantiation occurred, false
* if the type was already present, and undefined if the type can't
* be satisfied (because it is not the name of a type or it is nested
* too deep.
*/
_maybeInstantiate(type) {
if (type.slice(0,3) === '...') {
type = type.slice(3)
}
if (!(type.includes('<'))) {
// Not a template, so just check if type exists
if (type in this.Types) return false // already there
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)
return this.instantiateTemplate(base, arg)
}
/* 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 * to typed-function implementations and inserts the result into plain
* object imps * object imps
@ -985,7 +1027,7 @@ export default class PocomathInstance {
} else { } else {
// can bundle up func, and grab its signature if need be // can bundle up func, and grab its signature if need be
let destination = this[func] let destination = this[func]
if (destination &&needsig) { if (destination && needsig) {
destination = this._pocoresolve(func, needsig) destination = this._pocoresolve(func, needsig)
} }
refs[dep] = destination refs[dep] = destination
@ -996,6 +1038,7 @@ export default class PocomathInstance {
imps[signature] = this._typed.referToSelf(self => { imps[signature] = this._typed.referToSelf(self => {
refs.self = self refs.self = self
const implementation = does(refs) const implementation = does(refs)
Object.defineProperty(implementation, 'name', {value: does.name})
// What are we going to do with the return type info in here? // What are we going to do with the return type info in here?
return implementation return implementation
}) })
@ -1171,16 +1214,17 @@ export default class PocomathInstance {
const otherTypeList = typeListOfSignature(otherSig) const otherTypeList = typeListOfSignature(otherSig)
if (typeList.length !== otherTypeList.length) continue if (typeList.length !== otherTypeList.length) continue
let allMatch = true let allMatch = true
let paramBound = 'ground'
for (let k = 0; k < typeList.length; ++k) { for (let k = 0; k < typeList.length; ++k) {
let myType = typeList[k] let myType = typeList[k]
let otherType = otherTypeList[k] let otherType = otherTypeList[k]
if (otherType === theTemplateParam) { if (otherType === theTemplateParam) {
otherTypeList[k] = 'ground' otherTypeList[k] = paramBound
otherType = 'ground' otherType = paramBound
} }
if (otherType === '...T') { if (otherType === restTemplateParam) {
otherTypeList[k] = '...ground' otherTypeList[k] = `...${paramBound}`
otherType = 'ground' otherType = paramBound
} }
const adjustedOtherType = otherType.replaceAll( const adjustedOtherType = otherType.replaceAll(
`<${theTemplateParam}>`, '') `<${theTemplateParam}>`, '')
@ -1190,6 +1234,13 @@ export default class PocomathInstance {
} }
if (myType.slice(0,3) === '...') myType = myType.slice(3) if (myType.slice(0,3) === '...') myType = myType.slice(3)
if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) if (otherType.slice(0,3) === '...') otherType = otherType.slice(3)
const otherBound = upperBounds.exec(otherType)
if (otherBound) {
paramBound = otherBound[2]
otherType = paramBound
otherTypeList[k] = otherBound[1].replaceAll(
theTemplateParam, paramBound)
}
if (otherType === 'any') continue if (otherType === 'any') continue
if (otherType === 'ground') continue if (otherType === 'ground') continue
if (!(otherType in this.Types)) { if (!(otherType in this.Types)) {
@ -1218,25 +1269,49 @@ export default class PocomathInstance {
return foundSig return foundSig
} }
_pocoresolve(name, sig, typedFunction) { _pocoFindSignature(name, sig, typedFunction) {
if (!this._typed.isTypedFunction(typedFunction)) { if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name] typedFunction = this[name]
} }
let result = undefined let result = undefined
if (!this._typed.isTypedFunction(typedFunction)) {
return result
}
try { try {
result = this._typed.find(typedFunction, sig, {exact: true}) result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch { } catch {
} }
if (result) return result if (result) return result
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) return this._typed.find(typedFunction, foundsig) if (foundsig) return this._typed.findSignature(typedFunction, foundsig)
// Make sure bundle is up-to-date: const wantTypes = typeListOfSignature(sig)
for (const [implSig, details]
of typedFunction._typedFunctionData.signatureMap) {
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
}
if (allMatched) return details
}
// Hmm, no luck. Make sure bundle is up-to-date and retry:
typedFunction = this[name] typedFunction = this[name]
try { try {
result = this._typed.find(typedFunction, sig) result = this._typed.findSignature(typedFunction, sig)
} catch { } catch {
} }
if (result) return result return result
}
_pocoresolve(name, sig, typedFunction) {
if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name]
}
const result = this._pocoFindSignature(name, sig, typedFunction)
if (result) return result.implementation
// total punt, revert to typed-function resolution on every call; // total punt, revert to typed-function resolution on every call;
// hopefully this happens rarely: // hopefully this happens rarely:
return typedFunction return typedFunction

View File

@ -8,6 +8,8 @@ export function subsetOfKeys(set, obj) {
/* Returns a list of the types mentioned in a typed-function signature */ /* Returns a list of the types mentioned in a typed-function signature */
export function typeListOfSignature(signature) { export function typeListOfSignature(signature) {
signature = signature.trim()
if (!signature) return []
return signature.split(',').map(s => s.trim()) return signature.split(',').map(s => s.trim())
} }

View File

@ -1,3 +1,8 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const add = {'number,number': () => (m,n) => m+n} export const add = {
// Note the below assumes that all subtypes of number that will be defined
// are closed under addition!
'T:number, T': ({T}) => Returns(T, (m,n) => m+n)
}

View File

@ -2,5 +2,5 @@ import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const negate = { export const negate = {
'T (= number': ({T}) => Returns(T, n => -n) 'T:number': ({T}) => Returns(T, n => -n)
} }

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
export * from './Types/number.mjs' export * from './Types/number.mjs'
export const zero = {number: () => () => 0} export const zero = {number: () => Returns('NumInt', () => 0)}

View File

@ -1,3 +1,4 @@
import Returns from '../core/Returns.mjs'
import {Complex} from '../complex/Types/Complex.mjs' import {Complex} from '../complex/Types/Complex.mjs'
/* Note we don't **export** any types here, so that only the options /* Note we don't **export** any types here, so that only the options
@ -5,17 +6,20 @@ import {Complex} from '../complex/Types/Complex.mjs'
*/ */
export const floor = { export const floor = {
bigint: () => x => x, /* Because Pocomath isn't part of typed-function, nor does it have access
NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or * to the real typed-function parse, we unfortunately can't coalesce the
'Complex<bigint>': () => x => x, // at least have access to the real * first several implementations into one entry with type
// typed-function parse, we unfortunately can't coalesce these into one * `bigint|NumInt|GaussianInteger` because then they couldn't
// entry with type `bigint|NumInt|GaussianInteger` because they couldn't * be separately activated
// be separately activated then */
bigint: () => Returns('bigint', x => x),
NumInt: () => Returns('NumInt', x => x),
'Complex<bigint>': () => Returns('Complex<bigint>', x => x),
number: ({'equalTT(number,number)': eq}) => n => { number: ({'equalTT(number,number)': eq}) => Returns('NumInt', n => {
if (eq(n, Math.round(n))) return Math.round(n) if (eq(n, Math.round(n))) return Math.round(n)
return Math.floor(n) return Math.floor(n)
}, }),
'Complex<T>': Complex.promoteUnary['Complex<T>'], 'Complex<T>': Complex.promoteUnary['Complex<T>'],
@ -25,6 +29,6 @@ export const floor = {
BigNumber: ({ BigNumber: ({
'round(BigNumber)': rnd, 'round(BigNumber)': rnd,
'equal(BigNumber,BigNumber)': eq 'equal(BigNumber,BigNumber)': eq
}) => x => eq(x,round(x)) ? round(x) : x.floor() }) => Returns('BigNumber', x => eq(x,round(x)) ? round(x) : x.floor())
} }

View File

@ -23,6 +23,17 @@ describe('The default full pocomath instance "math"', () => {
it('can determine the return types of operations', () => { it('can determine the return types of operations', () => {
assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number') assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number')
assert.strictEqual(math.returnTypeOf('negate', 'NumInt'), 'NumInt') assert.strictEqual(math.returnTypeOf('negate', 'NumInt'), 'NumInt')
math.negate(math.complex(1.2, 2.8)) // TODO: make this call unnecessary
assert.strictEqual(
math.returnTypeOf('negate', 'Complex<number>'), 'Complex<number>')
assert.strictEqual(math.returnTypeOf('add', 'number,number'), 'number')
assert.strictEqual(math.returnTypeOf('add', 'NumInt,NumInt'), 'NumInt')
assert.strictEqual(math.returnTypeOf('add', 'NumInt,number'), 'number')
assert.strictEqual(math.returnTypeOf('add', 'number,NumInt'), 'number')
assert.deepStrictEqual( // TODO: ditto
math.add(3, math.complex(2.5, 1)), math.complex(5.5, 1))
assert.strictEqual(
math.returnTypeOf('add', 'Complex<number>,NumInt'), 'Complex<number>')
}) })
it('can subtract numbers', () => { it('can subtract numbers', () => {

8
test/core/_utils.mjs Normal file
View File

@ -0,0 +1,8 @@
import assert from 'assert'
import * as utils from '../../src/core/utils.mjs'
describe('typeListOfSignature', () => {
it('returns an empty list for the empty signature', () => {
assert.deepStrictEqual(utils.typeListOfSignature(''), [])
})
})