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 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'
const Complex = new PocomathInstance('Complex')
@ -21,7 +22,12 @@ Complex.installType('Complex<T>', {
})
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}

View File

@ -1,8 +1,10 @@
import Returns from '../core/Returns.mjs'
export * from './Types/Complex.mjs'
export const add = {
'Complex<T>,Complex<T>': ({
T,
'self(T,T)': me,
'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 '../generic/Types/generic.mjs'
@ -6,15 +7,16 @@ export const complex = {
* have a numeric/scalar type, e.g. by implementing subtypes in
* typed-function
*/
'undefined': () => u => u,
'undefined,any': () => (u, y) => u,
'any,undefined': () => (x, u) => u,
'undefined,undefined': () => (u, v) => u,
'T,T': () => (x, y) => ({re: x, im: y}),
'undefined': () => Returns('undefined', u => u),
'undefined,any': () => Returns('undefined', (u, y) => u),
'any,undefined': () => Returns('undefined', (x, u) => u),
'undefined,undefined': () => Returns('undefined', (u, v) => u),
'T,T': ({T}) => Returns(`Complex<${T}>`, (x, y) => ({re: x, im: y})),
/* Take advantage of conversions in typed-function */
// 'Complex<T>': () => z => z
/* But help out because without templates built in to typed-function,
* 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 theTemplateParam = 'T' // First pass: only allow this one exact parameter
const restTemplateParam = `...${theTemplateParam}`
const templateFromParam = 'U' // For defining covariant conversions
/* Returns a new signature just like sig but with the parameter replaced by
* the type
*/
const upperBounds = /\s*[(]=\s*(\w*)\s*/g
const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g
function substituteInSignature(signature, parameter, type) {
const sig = signature.replaceAll(upperBounds, '')
const sig = signature.replaceAll(upperBounds, '$1')
const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
return sig.replaceAll(pattern, type)
}
@ -53,6 +54,14 @@ export default class PocomathInstance {
this._typed = typed.create()
this._typed.clear()
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
* 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 */
returnTypeOf(operation, signature) {
if (typeof operation === 'string') {
operation = this[operation]
for (const type of typeListOfSignature(signature)) {
this._maybeInstantiate(type)
}
if (!(this._typed.isTypedFunction(operation))) return 'any'
const details = this._typed.findSignature(operation, signature)
if (typeof operation !== 'string') {
operation = operation.name
}
const details = this._pocoFindSignature(operation, signature)
return returnTypeOf(details.fn, signature, this)
}
@ -650,7 +661,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(/[<>(=\s]/)) {
for (const word of type.split(/[<>:\s]/)) {
if (this._templateParam(word)) {
explicit = false
break
@ -667,7 +678,7 @@ export default class PocomathInstance {
upperBounds.lastIndex = 0
let ubType = upperBounds.exec(rawSignature)
if (ubType) {
ubType = ubType[1]
ubType = ubType[2]
if (!ubType in this.Types) {
throw new TypeError(
`Unknown type upper bound '${ubType}' in '${rawSignature}'`)
@ -681,7 +692,9 @@ export default class PocomathInstance {
let instantiationSet = new Set()
for (const instType of behavior.instantiations) {
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)
}
}
@ -828,6 +841,7 @@ export default class PocomathInstance {
/* Generate the list of actual wanted types */
const wantTypes = parTypes.map(type => substituteInSignature(
type, theTemplateParam, instantiateFor))
const wantSig = wantTypes.join(',')
/* Now we have to add any actual types that are relevant
* to this invocation. Namely, that would be every formal parameter
* type in the invocation, with the parameter template instantiated
@ -890,9 +904,22 @@ export default class PocomathInstance {
}
// Finally ready to make the call.
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)
}
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:
const outerUses = new Set(Object.values(simplifiedUses))
this._addTFimplementation(
@ -905,19 +932,11 @@ export default class PocomathInstance {
const badSigs = new Set()
for (const sig in tf_imps) {
for (const type of typeListOfSignature(sig)) {
if (type.includes('<')) {
// 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)
if (base.slice(0,3) === '...') {
base = base.slice(3)
}
if (this.instantiateTemplate(base, arg) === undefined) {
if (this._maybeInstantiate(type) === undefined) {
badSigs.add(sig)
}
}
}
}
for (const badSig of badSigs) {
delete tf_imps[badSig]
}
@ -926,6 +945,29 @@ export default class PocomathInstance {
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
* to typed-function implementations and inserts the result into plain
* object imps
@ -985,7 +1027,7 @@ export default class PocomathInstance {
} else {
// can bundle up func, and grab its signature if need be
let destination = this[func]
if (destination &&needsig) {
if (destination && needsig) {
destination = this._pocoresolve(func, needsig)
}
refs[dep] = destination
@ -996,6 +1038,7 @@ export default class PocomathInstance {
imps[signature] = this._typed.referToSelf(self => {
refs.self = self
const implementation = does(refs)
Object.defineProperty(implementation, 'name', {value: does.name})
// What are we going to do with the return type info in here?
return implementation
})
@ -1171,16 +1214,17 @@ export default class PocomathInstance {
const otherTypeList = typeListOfSignature(otherSig)
if (typeList.length !== otherTypeList.length) continue
let allMatch = true
let paramBound = 'ground'
for (let k = 0; k < typeList.length; ++k) {
let myType = typeList[k]
let otherType = otherTypeList[k]
if (otherType === theTemplateParam) {
otherTypeList[k] = 'ground'
otherType = 'ground'
otherTypeList[k] = paramBound
otherType = paramBound
}
if (otherType === '...T') {
otherTypeList[k] = '...ground'
otherType = 'ground'
if (otherType === restTemplateParam) {
otherTypeList[k] = `...${paramBound}`
otherType = paramBound
}
const adjustedOtherType = otherType.replaceAll(
`<${theTemplateParam}>`, '')
@ -1190,6 +1234,13 @@ export default class PocomathInstance {
}
if (myType.slice(0,3) === '...') myType = myType.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 === 'ground') continue
if (!(otherType in this.Types)) {
@ -1218,25 +1269,49 @@ export default class PocomathInstance {
return foundSig
}
_pocoresolve(name, sig, typedFunction) {
_pocoFindSignature(name, sig, typedFunction) {
if (!this._typed.isTypedFunction(typedFunction)) {
typedFunction = this[name]
}
let result = undefined
if (!this._typed.isTypedFunction(typedFunction)) {
return result
}
try {
result = this._typed.find(typedFunction, sig, {exact: true})
result = this._typed.findSignature(typedFunction, sig, {exact: true})
} catch {
}
if (result) return result
const foundsig = this._findSubtypeImpl(name, this._imps[name], sig)
if (foundsig) return this._typed.find(typedFunction, foundsig)
// Make sure bundle is up-to-date:
if (foundsig) return this._typed.findSignature(typedFunction, foundsig)
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]
try {
result = this._typed.find(typedFunction, sig)
result = this._typed.findSignature(typedFunction, sig)
} 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;
// hopefully this happens rarely:
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 */
export function typeListOfSignature(signature) {
signature = signature.trim()
if (!signature) return []
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 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 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 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'
/* 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 = {
bigint: () => x => x,
NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or
'Complex<bigint>': () => x => x, // at least have access to the real
// typed-function parse, we unfortunately can't coalesce these into one
// entry with type `bigint|NumInt|GaussianInteger` because they couldn't
// be separately activated then
/* Because Pocomath isn't part of typed-function, nor does it have access
* to the real typed-function parse, we unfortunately can't coalesce the
* first several implementations into one entry with type
* `bigint|NumInt|GaussianInteger` because then they couldn't
* be separately activated
*/
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)
return Math.floor(n)
},
}),
'Complex<T>': Complex.promoteUnary['Complex<T>'],
@ -25,6 +29,6 @@ export const floor = {
BigNumber: ({
'round(BigNumber)': rnd,
'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', () => {
assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number')
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', () => {

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(''), [])
})
})