feat(returnTypeOf): Support template and upper bound types
Also changed the notation for an upper bound template to the more readable 'T:number' (instead of 'T (= number', which was supposed to look like the non-strict subset relation).
This commit is contained in:
parent
f7bb3697ed
commit
775bb9ddb7
@ -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)}
|
||||
|
@ -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}
|
||||
|
@ -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)))
|
||||
}
|
||||
|
@ -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)}))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)}
|
||||
|
@ -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())
|
||||
|
||||
}
|
||||
|
@ -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
8
test/core/_utils.mjs
Normal 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(''), [])
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user