diff --git a/src/bigint/absquare.mjs b/src/bigint/absquare.mjs index 4c2040a..587e8ec 100644 --- a/src/bigint/absquare.mjs +++ b/src/bigint/absquare.mjs @@ -1,6 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' /* Absolute value squared */ export const absquare = { - bigint: ({'square(bigint)': sqb}) => b => sqb(b) + bigint: ({'square(bigint)': sqb}) => Returns('bigint', b => sqb(b)) } diff --git a/src/bigint/add.mjs b/src/bigint/add.mjs index 1cd296d..c4291ac 100644 --- a/src/bigint/add.mjs +++ b/src/bigint/add.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const add = {'bigint,bigint': () => (a,b) => a+b} +export const add = {'bigint,bigint': () => Returns('bigint', (a,b) => a+b)} diff --git a/src/bigint/compare.mjs b/src/bigint/compare.mjs index ab830ab..097dfca 100644 --- a/src/bigint/compare.mjs +++ b/src/bigint/compare.mjs @@ -1,5 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const compare = { - 'bigint,bigint': () => (a,b) => a === b ? 0n : (a > b ? 1n : -1n) + 'bigint,bigint': () => Returns( + 'boolean', (a,b) => a === b ? 0n : (a > b ? 1n : -1n)) } diff --git a/src/bigint/divide.mjs b/src/bigint/divide.mjs index 4554457..492893a 100644 --- a/src/bigint/divide.mjs +++ b/src/bigint/divide.mjs @@ -1,12 +1,13 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const divide = { 'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => { - if (config.predictable) return quot - return (n, d) => { + if (config.predictable) return Returns('bigint', (n,d) => quot(n,d)) + return Returns('bigint|undefined', (n, d) => { const q = n/d if (q * d == n) return q return undefined - } + }) } } diff --git a/src/bigint/isZero.mjs b/src/bigint/isZero.mjs index 0efa71c..02aca57 100644 --- a/src/bigint/isZero.mjs +++ b/src/bigint/isZero.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const isZero = {bigint: () => b => b === 0n} +export const isZero = {bigint: () => Returns('boolean', b => b === 0n)} diff --git a/src/bigint/multiply.mjs b/src/bigint/multiply.mjs index e19959e..e80cee0 100644 --- a/src/bigint/multiply.mjs +++ b/src/bigint/multiply.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const multiply = {'bigint,bigint': () => (a,b) => a*b} +export const multiply = {'bigint,bigint': () => Returns('bigint', (a,b) => a*b)} diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index 6cc76d4..b387615 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -1,12 +1,12 @@ import gcdType from '../generic/gcdType.mjs' -import {identity} from '../generic/identity.mjs' +import {identityType} from '../generic/identity.mjs' export * from './Types/bigint.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' -export const conjugate = {bigint: () => identity} +export const conjugate = {bigint: identityType('bigint')} export {divide} from './divide.mjs' export const gcd = gcdType('bigint') export {isZero} from './isZero.mjs' diff --git a/src/bigint/negate.mjs b/src/bigint/negate.mjs index d44cdb0..ecd51f1 100644 --- a/src/bigint/negate.mjs +++ b/src/bigint/negate.mjs @@ -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)} diff --git a/src/bigint/one.mjs b/src/bigint/one.mjs index f548a65..8e8a7f2 100644 --- a/src/bigint/one.mjs +++ b/src/bigint/one.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const one = {bigint: () => () => 1n} +export const one = {bigint: () => Returns('bigint', () => 1n)} diff --git a/src/bigint/quotient.mjs b/src/bigint/quotient.mjs index 589adc3..c1a086a 100644 --- a/src/bigint/quotient.mjs +++ b/src/bigint/quotient.mjs @@ -1,13 +1,14 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -/* Returns the best integer approximation to n/d */ +/* Returns the floor integer approximation to n/d */ export const quotient = { - 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + 'bigint,bigint': ({'sign(bigint)': sgn}) => Returns('bigint', (n, d) => { const dSgn = sgn(d) if (dSgn === 0n) return 0n if (sgn(n) === dSgn) return n/d const quot = n/d if (quot * d == n) return quot return quot - 1n - } + }) } diff --git a/src/bigint/roundquotient.mjs b/src/bigint/roundquotient.mjs index c6763a2..57fb941 100644 --- a/src/bigint/roundquotient.mjs +++ b/src/bigint/roundquotient.mjs @@ -1,8 +1,9 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' /* Returns the closest integer approximation to n/d */ export const roundquotient = { - 'bigint,bigint': ({'sign(bigint)': sgn}) => (n, d) => { + 'bigint,bigint': ({'sign(bigint)': sgn}) => Returns('bigint', (n, d) => { const dSgn = sgn(d) if (dSgn === 0n) return 0n const candidate = n/d @@ -11,5 +12,5 @@ export const roundquotient = { if (2n * rem > absd) return candidate + dSgn if (-2n * rem >= absd) return candidate - dSgn return candidate - } + }) } diff --git a/src/bigint/sign.mjs b/src/bigint/sign.mjs index af48e05..c811df2 100644 --- a/src/bigint/sign.mjs +++ b/src/bigint/sign.mjs @@ -1,9 +1,10 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' export const sign = { - bigint: () => b => { + bigint: () => Returns('bigint', b => { if (b === 0n) return 0n if (b > 0n) return 1n return -1n - } + }) } diff --git a/src/bigint/sqrt.mjs b/src/bigint/sqrt.mjs index 4d34513..01ef0b0 100644 --- a/src/bigint/sqrt.mjs +++ b/src/bigint/sqrt.mjs @@ -1,5 +1,6 @@ -export * from './Types/bigint.mjs' +import Returns from '../core/Returns.mjs' import isqrt from 'bigint-isqrt' +export * from './Types/bigint.mjs' export const sqrt = { bigint: ({ @@ -11,18 +12,18 @@ export const sqrt = { // Don't just return the constant isqrt here because the object // gets decorated with info that might need to be different // for different PocomathInstancss - return b => isqrt(b) + return Returns('bigint', b => isqrt(b)) } if (!cplx) { - return b => { + return Returns('bigint|undefined', b => { if (b >= 0n) { const trial = isqrt(b) if (trial * trial === b) return trial } return undefined - } + }) } - return b => { + return Returns('bigint|Complex|undefined', b => { if (b === undefined) return undefined let real = true if (b < 0n) { @@ -33,6 +34,6 @@ export const sqrt = { if (trial * trial !== b) return undefined if (real) return trial return cplx(0n, trial) - } + }) } } diff --git a/src/bigint/zero.mjs b/src/bigint/zero.mjs index 0c63a1a..e9fe83b 100644 --- a/src/bigint/zero.mjs +++ b/src/bigint/zero.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/bigint.mjs' -export const zero = {bigint: () => () => 0n} +export const zero = {bigint: () => Returns('bigint', () => 0n)} diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 0fa4107..4d63417 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,15 +1,14 @@ +import {Returns, returnTypeOf} from '../../core/Returns.mjs' import PocomathInstance from '../../core/PocomathInstance.mjs' const Complex = new PocomathInstance('Complex') -// Base type that should generally not be used directly -Complex.installType('Complex', { - test: z => z && typeof z === 'object' && 're' in z && 'im' in z -}) -// Now the template type: Complex numbers are actually always homeogeneous -// in their component types. +// Now the template type: Complex numbers are actually always homogeneous +// in their component types. For an explanation of the meanings of the +// properties, see ../../tuple/Types/Tuple.mjs Complex.installType('Complex', { - infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), + base: z => z && typeof z === 'object' && 're' in z && 'im' in z, test: testT => z => testT(z.re) && testT(z.im), + infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), from: { T: t => ({re: t, im: t-t}), // hack: maybe need a way to call zero(T) U: convert => u => { @@ -21,7 +20,12 @@ Complex.installType('Complex', { }) Complex.promoteUnary = { - 'Complex': ({'self(T)': me, complex}) => z => complex(me(z.re), me(z.im)) + 'Complex': ({ + T, + 'self(T)': me, + complex + }) => Returns( + `Complex<${returnTypeOf(me)}>`, z => complex(me(z.re), me(z.im))) } export {Complex} diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 536b8b4..53a2464 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,10 +1,21 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const abs = { 'Complex': ({ - sqrt, // Calculation of the type needed in the square root (the - // underlying numeric type of T, whatever T is, is beyond Pocomath's - // (current) template abilities, so punt and just do full resolution + T, + sqrt, // Unfortunately no notation yet for the needed signature + 'absquare(T)': baseabsq, 'absquare(Complex)': absq - }) => z => sqrt(absq(z)) + }) => { + const midType = returnTypeOf(baseabsq) + const sqrtImp = sqrt.fromInstance.resolve('sqrt', midType, sqrt) + let retType = returnTypeOf(sqrtImp) + if (retType.includes('|')) { + // This is a bit of a hack, as it relies on all implementations of + // sqrt returning the "typical" return type as the first option + retType = retType.split('|',1)[0] + } + return Returns(retType, z => sqrtImp(absq(z))) + } } diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs index bb4677f..ab0194c 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -1,9 +1,27 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const absquare = { 'Complex': ({ - add, // Calculation of exact type needed in add (underlying numeric of T) - // is (currently) too involved for Pocomath + add, // no easy way to write the needed signature; if T is number + // it is number,number; but if T is Complex, it is just + // bigint,bigint. So unfortunately we depend on all of add, and + // we extract the needed implementation below. 'self(T)': absq - }) => z => add(absq(z.re), absq(z.im)) + }) => { + const midType = returnTypeOf(absq) + const addImp = add.fromInstance.resolve( + 'add', `${midType},${midType}`, add) + return Returns( + returnTypeOf(addImp), z => addImp(absq(z.re), absq(z.im))) + } } + +/* We could imagine notations that Pocomath could support that would simplify + * the above, maybe something like + * 'Complex': ({ + * 'self(T): U': absq, + * 'add(U,U):V': plus, + * V + * }) => Returns(V, z => plus(absq(z.re), absq(z.im))) + */ diff --git a/src/complex/add.mjs b/src/complex/add.mjs index 9afdd90..6172326 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -1,8 +1,10 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const add = { 'Complex,Complex': ({ + 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))) } diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs index 10c799c..9aae6e1 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' /* Returns true if w is z multiplied by a complex unit */ @@ -9,9 +10,9 @@ export const associate = { 'one(T)': uno, 'complex(T,T)': cplx, 'negate(Complex)': neg - }) => (w,z) => { + }) => Returns('boolean', (w,z) => { if (eq(w,z) || eq(w,neg(z))) return true const ti = times(z, cplx(zr(z.re), uno(z.im))) return eq(w,ti) || eq(w,neg(ti)) - } + }) } diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index a5a24f5..49cfa60 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -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': () => 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)})) } diff --git a/src/complex/conjugate.mjs b/src/complex/conjugate.mjs index b94495d..c81180e 100644 --- a/src/complex/conjugate.mjs +++ b/src/complex/conjugate.mjs @@ -1,9 +1,11 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const conjugate = { 'Complex': ({ + T, 'negate(T)': neg, 'complex(T,T)': cplx - }) => z => cplx(z.re, neg(z.im)) + }) => Returns(`Complex<${T}>`, z => cplx(z.re, neg(z.im))) } diff --git a/src/complex/equalTT.mjs b/src/complex/equalTT.mjs index 6899aa0..6a84c9a 100644 --- a/src/complex/equalTT.mjs +++ b/src/complex/equalTT.mjs @@ -1,9 +1,11 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const equalTT = { 'Complex,Complex': ({ + T, 'self(T,T)': me - }) => (w,z) => me(w.re, z.re) && me(w.im, z.im), + }) => Returns('boolean', (w, z) => me(w.re, z.re) && me(w.im, z.im)), // NOTE: Although I do not understand exactly why, with typed-function@3.0's // matching algorithm, the above template must come first to ensure the // most specific match to a template call. I.e, if one of the below @@ -11,16 +13,16 @@ export const equalTT = { // with (Complex>, Complex) (!, hopefully in some // future iteration typed-function will be smart enough to prefer // Complex, Complex. Possibly the problem is in Pocomath's bolted-on - // type resolution and the difficulty will go away when features are moved into - // typed-function. + // type resolution and the difficulty will go away when features are moved + // into typed-function. 'Complex,T': ({ 'isZero(T)': isZ, 'self(T,T)': eqReal - }) => (z, x) => eqReal(z.re, x) && isZ(z.im), + }) => Returns('boolean', (z, x) => eqReal(z.re, x) && isZ(z.im)), 'T,Complex': ({ 'isZero(T)': isZ, 'self(T,T)': eqReal - }) => (b, z) => eqReal(z.re, b) && isZ(z.im), + }) => Returns('boolean', (b, z) => eqReal(z.re, b) && isZ(z.im)), } diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs index 30b7dad..be90e4a 100644 --- a/src/complex/gcd.mjs +++ b/src/complex/gcd.mjs @@ -1,5 +1,6 @@ import PocomathInstance from '../core/PocomathInstance.mjs' -import * as Complex from './Types/Complex.mjs' +import Returns from '../core/Returns.mjs' +import * as Complex from './Types/Complex.mjs' import gcdType from '../generic/gcdType.mjs' const gcdComplexRaw = {} @@ -9,15 +10,16 @@ const imps = { gcdComplexRaw, gcd: { // Only return gcds with positive real part 'Complex,Complex': ({ + T, 'gcdComplexRaw(Complex,Complex)': gcdRaw, 'sign(T)': sgn, 'one(T)': uno, 'negate(Complex)': neg - }) => (z,m) => { + }) => Returns(`Complex<${T}>`, (z,m) => { const raw = gcdRaw(z, m) if (sgn(raw.re) === uno(raw.re)) return raw return neg(raw) - } + }) } } diff --git a/src/complex/invert.mjs b/src/complex/invert.mjs index 2f68e43..ce1b932 100644 --- a/src/complex/invert.mjs +++ b/src/complex/invert.mjs @@ -1,14 +1,16 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const invert = { 'Complex': ({ + T, 'conjugate(Complex)': conj, 'absquare(Complex)': asq, 'complex(T,T)': cplx, 'divide(T,T)': div - }) => z => { + }) => Returns(`Complex<${T}>`, z => { const c = conj(z) const d = asq(z) return cplx(div(c.re, d), div(c.im, d)) - } + }) } diff --git a/src/complex/isZero.mjs b/src/complex/isZero.mjs index 01a2f51..3e10c9b 100644 --- a/src/complex/isZero.mjs +++ b/src/complex/isZero.mjs @@ -1,5 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const isZero = { - 'Complex': ({'self(T)': me}) => z => me(z.re) && me(z.im) + 'Complex': ({'self(T)': me}) => Returns( + 'boolean', z => me(z.re) && me(z.im)) } diff --git a/src/complex/multiply.mjs b/src/complex/multiply.mjs index e059a91..5d9edc6 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -1,15 +1,20 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const multiply = { 'Complex,Complex': ({ + T, 'complex(T,T)': cplx, 'add(T,T)': plus, - 'subtract(T,T)': sub, + 'subtract(T,T)': subt, 'self(T,T)': me, 'conjugate(T)': conj // makes quaternion multiplication work - }) => (w,z) => { - return cplx( - sub(me(w.re, z.re), me(conj(w.im), z.im)), - plus(me(conj(w.re), z.im), me(w.im, z.re))) - } + }) => Returns( + `Complex<${T}>`, + (w,z) => { + const realpart = subt(me( w.re, z.re), me(conj(w.im), z.im)) + const imagpart = plus(me(conj(w.re), z.im), me( w.im, z.re)) + return cplx(realpart, imagpart) + } + ) } diff --git a/src/complex/quaternion.mjs b/src/complex/quaternion.mjs index 4f35d30..a435395 100644 --- a/src/complex/quaternion.mjs +++ b/src/complex/quaternion.mjs @@ -1,5 +1,14 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' +// Might be nice to have type aliases! export const quaternion = { - 'T,T,T,T': ({complex}) => (r,i,j,k) => complex(complex(r,j), complex(i,k)) + 'T,T,T,T': ({ + T, + 'complex(T,T)': cplxT, + 'complex(Complex,Complex)': quat + }) => Returns( + `Complex>`, + (r,i,j,k) => quat(cplxT(r,j), cplxT(i,k)) + ) } diff --git a/src/complex/quotient.mjs b/src/complex/quotient.mjs index 32299ca..6b53de1 100644 --- a/src/complex/quotient.mjs +++ b/src/complex/quotient.mjs @@ -1,7 +1,9 @@ +import Returns from '../core/Returns.mjs' export * from './roundquotient.mjs' export const quotient = { 'Complex,Complex': ({ + T, 'roundquotient(Complex,Complex)': rq - }) => (w,z) => rq(w,z) + }) => Returns(`Complex<${T}>`, (w,z) => rq(w,z)) } diff --git a/src/complex/roundquotient.mjs b/src/complex/roundquotient.mjs index 5c25765..474077f 100644 --- a/src/complex/roundquotient.mjs +++ b/src/complex/roundquotient.mjs @@ -1,17 +1,19 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const roundquotient = { 'Complex,Complex': ({ + T, 'isZero(Complex)': isZ, 'conjugate(Complex)': conj, 'multiply(Complex,Complex)': mult, 'absquare(Complex)': asq, 'self(T,T)': me, 'complex(T,T)': cplx - }) => (n,d) => { + }) => Returns(`Complex<${T}>`, (n,d) => { if (isZ(d)) return d const cnum = mult(n, conj(d)) const dreal = asq(d) return cplx(me(cnum.re, dreal), me(cnum.im, dreal)) - } + }) } diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs index d60ed6a..7f4044b 100644 --- a/src/complex/sqrt.mjs +++ b/src/complex/sqrt.mjs @@ -1,3 +1,4 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const sqrt = { @@ -12,29 +13,41 @@ export const sqrt = { 'multiply(T,T)': mult, 'self(T)': me, 'divide(T,T)': div, - 'abs(Complex)': absC, + 'absquare(Complex)': absqC, 'subtract(T,T)': sub }) => { + let baseReturns = returnTypeOf(me) + if (baseReturns.includes('|')) { + // Bit of a hack, because it is relying on other implementations + // to list the "typical" value of sqrt first + baseReturns = baseReturns.split('|', 1)[0] + } + if (config.predictable) { - return z => { + return Returns(`Complex<${baseReturns}>`, z => { const reOne = uno(z.re) if (isZ(z.im) && sgn(z.re) === reOne) return cplxU(me(z.re)) const reTwo = plus(reOne, reOne) + const myabs = me(absqC(z)) return cplxB( - mult(sgn(z.im), me(div(plus(absC(z),z.re), reTwo))), - me(div(sub(absC(z),z.re), reTwo)) + mult(sgn(z.im), me(div(plus(myabs, z.re), reTwo))), + me(div(sub(myabs, z.re), reTwo)) ) + }) + } + + return Returns( + `Complex<${baseReturns}>|${baseReturns}|undefined`, + z => { + const reOne = uno(z.re) + if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) + const reTwo = plus(reOne, reOne) + const myabs = me(absqC(z)) + const reSqrt = me(div(plus(myabs, z.re), reTwo)) + const imSqrt = me(div(sub(myabs, z.re), reTwo)) + if (reSqrt === undefined || imSqrt === undefined) return undefined + return cplxB(mult(sgn(z.im), reSqrt), imSqrt) } - } - return z => { - const reOne = uno(z.re) - if (isZ(z.im) && sgn(z.re) === reOne) return me(z.re) - const reTwo = plus(reOne, reOne) - const reSqrt = me(div(plus(absC(z),z.re), reTwo)) - const imSqrt = me(div(sub(absC(z),z.re), reTwo)) - if (reSqrt === undefined || imSqrt === undefined) return undefined - return cplxB(mult(sgn(z.im), reSqrt), imSqrt) - } + ) } } - diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 1add7eb..4b46752 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,22 +1,94 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' -import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' +import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' +import {Returns, returnTypeOf} from './Returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type +/* Template/signature parsing stuff; should probably be moved to a + * separate file, but it's a bit interleaved at the moment + */ + const theTemplateParam = 'T' // First pass: only allow this one exact parameter +const restTemplateParam = `...${theTemplateParam}` +const templateCall = `<${theTemplateParam}>` const templateFromParam = 'U' // For defining covariant conversions +/* returns the pair [base, instance] for a template type. If the type + * is not a template, instance is undefined + */ +const templatePattern = /^\s*([^<\s]*)\s*<\s*(\S*)\s*>\s*$/ +function splitTemplate(type) { + if (!(type.includes('<'))) return [type, undefined] + const results = templatePattern.exec(type) + return [results[1], results[2]] +} +/* Returns the instance such that type is template instantiated for that + * instance. + */ +function whichInstance(type, template) { + if (template === theTemplateParam) return type + if (type === template) return '' + if (!(template.includes(templateCall))) { + throw new TypeError( + `Type ${template} is not a template, so can't produce ${type}`) + } + const [typeBase, typeInstance] = splitTemplate(type) + if (!typeInstance) { + throw new TypeError( + `Type ${type} not from a template, so isn't instance of ${template}`) + } + const [tempBase, tempInstance] = splitTemplate(template) + if (typeBase !== tempBase) { + throw new TypeError( + `Type ${type} has wrong top-level base to be instance of ${template}`) + } + return whichInstance(typeInstance, tempInstance) +} +/* Same as above, but for signatures */ +function whichSigInstance(sig, tempsig) { + const sigTypes = typeListOfSignature(sig) + const tempTypes = typeListOfSignature(tempsig) + const sigLength = sigTypes.length + if (sigLength === 0) { + throw new TypeError("No types in signature, so can't determine instance") + } + if (sigLength !== tempTypes.length) { + throw new TypeError(`Signatures ${sig} and ${tempsig} differ in length`) + } + let maybeInstance = whichInstance(sigTypes[0], tempTypes[0]) + for (let i = 1; i < sigLength; ++i) { + const currInstance = whichInstance(sigTypes[i], tempTypes[i]) + if (maybeInstance) { + if (currInstance && currInstance !== maybeInstance) { + throw new TypeError( + `Inconsistent instantiation of ${sig} from ${tempsig}`) + } + } else { + maybeInstance = currInstance + } + } + if (!maybeInstance) { + throw new TypeError( + `Signature ${sig} identical to ${tempsig}, not an instance`) + } + return maybeInstance +} + /* Returns a new signature just like sig but with the parameter replaced by * the type */ -function substituteInSig(sig, parameter, type) { +const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g +function substituteInSignature(signature, parameter, type) { + const sig = signature.replaceAll(upperBounds, '$1') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } +let lastWhatToDo = null // used in an infinite descent check + export default class PocomathInstance { /* Disallowed names for ops; beware, this is slightly non-DRY * in that if a new top-level PocomathInstance method is added, its name @@ -29,9 +101,15 @@ export default class PocomathInstance { 'install', 'installType', 'instantiateTemplate', + 'isPriorTo', + 'isSubtypeOf', 'joinTypes', 'name', + 'returnTypeOf', + 'resolve', 'self', + 'subtypesOf', + 'supertypesOf', 'Templates', 'typeOf', 'Types', @@ -40,22 +118,47 @@ export default class PocomathInstance { constructor(name) { this.name = name - this._imps = {} + this._imps = {} // Pocomath implementations, with dependencies + this._TFimps = {} // typed-function implementations, dependencies resolved this._affects = {} this._typed = typed.create() this._typed.clear() - this._typed.addTypes([{name: 'ground', test: () => true}]) - /* List of types installed in the instance. We start with just dummies - * for the 'any' type and for type parameters: - */ + // The following is an additional typed-function universe for resolving + // uninstantiated template instances. It is linked to the main one in + // its onMismatch function, below: + this._metaTyped = typed.create() + this._metaTyped.clear() + // And these are the meta bindings: (I think we don't need separate + // invalidation for them as they are only accessed through a main call.) + this._meta = {} // The resulting typed-functions + this._metaTFimps = {} // and their implementations + const me = this + const myTyped = this._typed + this._typed.onMismatch = (name, args, sigs) => { + if (me._invalid.has(name)) { + // rebuild implementation and try again + return me[name](...args) + } + const metaversion = me._meta[name] + if (metaversion) { + return me._meta[name](...args) + } + me._typed.throwMismatchError(name, args, sigs) + } + // List of types installed in the instance: (We start with just dummies + // for the 'any' type and for type parameters.) this.Types = {any: anySpec} this.Types[theTemplateParam] = anySpec - this.Types.ground = anySpec - // All the template types that have been defined + // Types that have been moved into the metaverse: + this._metafiedTypes = new Set() + // All the template types that have been defined: this.Templates = {} - // The actual type testing functions + // And their instantiations: + this._instantiationsOf = {} + // The actual type testing functions: this._typeTests = {} - this._subtypes = {} // For each type, gives all of its (in)direct subtypes + // For each type, gives all of its (in)direct subtypes in topo order: + this._subtypes = {} /* The following gives for each type, a set of all types that could * match in typed-function's dispatch algorithm before the given type. * This is important because if we instantiate a template, we must @@ -67,25 +170,19 @@ export default class PocomathInstance { this._maxDepthSeen = 1 // deepest template nesting we've actually encountered this._invalid = new Set() // methods that are currently invalid this._config = {predictable: false, epsilon: 1e-12} - const self = this this.config = new Proxy(this._config, { get: (target, property) => target[property], set: (target, property, value) => { if (value !== target[property]) { target[property] = value - self._invalidateDependents('config') + me._invalidateDependents('config') } return true // successful } }) this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions - - this._installFunctions({ - typeOf: {ground: {uses: new Set(), does: () => () => 'any'}} - }) - - this.joinTypes = this.joinTypes.bind(this) + this.joinTypes = this.joinTypes.bind(me) } /** @@ -144,7 +241,7 @@ export default class PocomathInstance { * instantiation can be accomplished by prefixin the signature with an * exclamation point. */ - install(ops) { + install = Returns('void', function(ops) { if (ops instanceof PocomathInstance) { return _installInstance(ops) } @@ -170,14 +267,17 @@ export default class PocomathInstance { const stdimps = {} for (const [signature, does] of Object.entries(spec)) { const uses = new Set() - does(dependencyExtractor(uses)) + try { + does(dependencyExtractor(uses)) + } catch { + } stdimps[signature] = {uses, does} } stdFunctions[item] = stdimps } } this._installFunctions(stdFunctions) - } + }) /* Merge any number of PocomathInstances or modules: */ static merge(name, ...pieces) { @@ -188,10 +288,28 @@ export default class PocomathInstance { return result } + /* Determine the return type of an operation given an input signature */ + returnTypeOf = Returns('string', function(operation, signature) { + for (const type of typeListOfSignature(signature)) { + this._maybeInstantiate(type) + } + if (typeof operation !== 'string') { + operation = operation.name + } + const details = this._pocoFindSignature(operation, signature) + if (details) { + return returnTypeOf(details.fn, signature, this) + } + return returnTypeOf(this[operation], signature, this) + }) + /* Return a chain object for this instance with a given value: */ - chain(value) { - return makeChain(value, this, this._chainRepository) - } + chain = Returns( + sig => `Chain<${sig}>`, + function(value) { + return makeChain(value, this, this._chainRepository) + } + ) _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { @@ -240,9 +358,9 @@ export default class PocomathInstance { for (const name of requiredSet) { for (const type of typeSet) { try { - const modName = `../${type}/${name}.mjs` - const mod = await import(modName) - this.install(mod) + const moduleName = `../${type}/${name}.mjs` + const module = await import(moduleName) + this.install(module) } catch (err) { if (!(err.message.includes('find'))) { // Not just a error because module doesn't exist @@ -283,16 +401,17 @@ export default class PocomathInstance { * Implementation note: unlike _installFunctions below, we can make * the corresponding changes to the _typed object immediately */ - installType(type, spec) { - const parts = type.split(/[<,>]/) + installType = Returns('void', function(type, spec) { + const parts = type.split(/[<,>]/).map(s => s.trim()) if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) } if (parts.some(this._templateParam.bind(this))) { - // It's a template, deal with it separately + // It's an uninstantiated template, deal with it separately return this._installTemplateType(type, spec) } + const base = parts[0] if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -305,7 +424,7 @@ export default class PocomathInstance { } let beforeType = spec.refines if (!beforeType) { - beforeType = 'ground' + beforeType = 'any' for (const other of spec.before || []) { if (other in this.Types) { beforeType = other @@ -321,14 +440,14 @@ export default class PocomathInstance { this._typeTests[type] = testFn this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec - this._subtypes[type] = new Set() + this._subtypes[type] = [] this._priorTypes[type] = new Set() // Update all the subtype sets of supertypes up the chain let nextSuper = spec.refines while (nextSuper) { this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) - this._subtypes[nextSuper].add(type) + this._addSubtypeTo(nextSuper, type) nextSuper = this.Types[nextSuper].refines } /* Now add conversions to this type */ @@ -347,6 +466,21 @@ export default class PocomathInstance { for (const subtype of this._subtypes[from]) { this._priorTypes[nextSuper].add(subtype) } + + /* Add the conversion in the metaverse if need be: */ + const [toBase, toInstance] = splitTemplate(nextSuper) + if (toInstance) { + const [fromBase, fromInstance] = splitTemplate(from) + if (!fromBase || fromBase !== toBase) { + this._metafy(from) + try { + this._metaTyped.addConversion( + {from, to: toBase, convert: spec.from[from]}) + } catch { + } + } + } + nextSuper = this.Types[nextSuper].refines } } @@ -356,8 +490,8 @@ export default class PocomathInstance { for (const fromtype in this.Types[to].from) { if (type == fromtype || (fromtype in this._subtypes - && this._subtypes[fromtype].has(type))) { - if (spec.refines == to || spec.refines in this._subtypes[to]) { + && this.isSubtypeOf(type, fromtype))) { + if (spec.refines == to || this.isSubtypeOf(spec.refines,to)) { throw new SyntaxError( `Conversion of ${type} to its supertype ${to} disallowed.`) } @@ -371,6 +505,16 @@ export default class PocomathInstance { convert: this.Types[to].from[fromtype] }) this._invalidateDependents(':' + nextSuper) + /* Add the conversion in the metaverse if need be: */ + const [toBase, toInstance] = splitTemplate(nextSuper) + if (toInstance && base !== toBase) { + this._metafy(type) + this._metaTyped.addConversion({ + from: type, + to: toBase, + convert: this.Types[to].from[fromtype] + }) + } } catch { } this._priorTypes[nextSuper].add(type) @@ -381,43 +525,92 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(), does: () => () => type} + imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) + }) + + _metafy(type) { + if (this._metafiedTypes.has(type)) return + this._metaTyped.addTypes([{name: type, test: this._typeTests[type]}]) + this._metafiedTypes.add(type) } - /* Returns the most refined type of all the types in the array, with - * '' standing for the empty type for convenience. If the second + _addSubtypeTo(sup, sub) { + if (this.isSubtypeOf(sub, sup)) return + const supSubs = this._subtypes[sup] + let i + for (i = 0; i < supSubs.length; ++i) { + if (this.isSubtypeOf(sub, supSubs[i])) break + } + supSubs.splice(i, 0, sub) + } + + /* Returns true if typeA is a strict subtype of type B */ + isSubtypeOf = Returns('boolean', function(typeA, typeB) { + // Currently not considering types to be a subtype of 'any' + if (typeB === 'any' || typeA === 'any') return false + return this._subtypes[typeB].includes(typeA) + }) + + /* Returns true if typeA is a subtype of or converts to type B */ + isPriorTo = Returns('boolean', function(typeA, typeB) { + if (!(typeB in this._priorTypes)) return false + return this._priorTypes[typeB].has(typeA) + }) + + /* Returns a list of the strict ubtypes of a given type, in topological + * sorted order (i.e, no type on the list contains one that comes after it). + */ + subtypesOf = Returns('Array', function(type) { + return this._subtypes[type] // should we clone? + }) + + /* Returns a list of the supertypes of a given type, starting with itself, + * in topological order + */ + supertypesOf = Returns('Array', function(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 * a subtype (defaults to false). */ - joinTypes(types, convert) { + joinTypes = Returns('string', function(types, convert) { let join = '' for (const type of types) { join = this._joinTypes(join, type, convert) } return join - } + }) + /* helper for above */ _joinTypes(typeA, typeB, convert) { if (!typeA) return typeB if (!typeB) return typeA if (typeA === 'any' || typeB === 'any') return 'any' - if (typeA === 'ground' || typeB === 'ground') return 'ground' if (typeA === typeB) return typeA const subber = convert ? this._priorTypes : this._subtypes - if (subber[typeB].has(typeA)) return typeB + const pick = convert ? 'has' : 'includes' + if (subber[typeB][pick](typeA)) return typeB /* OK, so we need the most refined supertype of A that contains B: */ let nextSuper = typeA while (nextSuper) { - if (subber[nextSuper].has(typeB)) return nextSuper + if (subber[nextSuper][pick](typeB)) return nextSuper nextSuper = this.Types[nextSuper].refines } /* And if conversions are allowed, we have to search the other way too */ if (convert) { nextSuper = typeB while (nextSuper) { - if (subber[nextSuper].has(typeA)) return nextSuper + if (subber[nextSuper][pick](typeA)) return nextSuper nextSuper = this.Types[nextSuper].refines } } @@ -427,13 +620,14 @@ export default class PocomathInstance { /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ - undefinedTypes() { - return Array.from(this._seenTypes).filter(t => !(t in this.Types)) - } + undefinedTypes = Returns('Array', function() { + return Array.from(this._seenTypes).filter( + t => !(t in this.Types || t in this.Templates)) + }) /* Used internally to install a template type */ _installTemplateType(type, spec) { - const base = type.split('<')[0] + const [base] = splitTemplate(type) /* For now, just allow a single template per base type; that * might need to change later: */ @@ -444,11 +638,29 @@ export default class PocomathInstance { } return } + + // install the "base type" in the meta universe: + let beforeType = 'any' + for (const other of spec.before || []) { + if (other in this.templates) { + beforeType = other + break + } + } + this._metaTyped.addTypes([{name: base, test: spec.base}], beforeType) + this._instantiationsOf[base] = new Set() + // update the typeOf function const imp = {} - imp[type] = {uses: new Set(['T']), does: ({T}) => () => `${base}<${T}>`} + imp[type] = { + uses: new Set(['T']), + does: ({T}) => Returns('string', () => `${base}<${T}>`) + } this._installFunctions({typeOf: imp}) + // Invalidate any functions that reference this template type: + this._invalidateDependents(':' + base) + // Nothing else actually happens until we match a template parameter this.Templates[base] = {type, spec} } @@ -480,8 +692,27 @@ export default class PocomathInstance { `Conflicting definitions of ${signature} for ${name}`) } } else { - // Must avoid aliasing into another instance: - opImps[signature] = {uses: behavior.uses, does: behavior.does} + /* Check if it's an ordinary non-template signature */ + let explicit = true + for (const type of typesOfSignature(signature)) { + for (const word of type.split(/[<>:\s]/)) { + if (this._templateParam(word)) { + explicit = false + break + } + } + if (!explicit) break + } + opImps[signature] = { + explicit, + resolved: false, + uses: behavior.uses, + does: behavior.does + } + if (!explicit) { + opImps[signature].hasInstantiations = {} + opImps[signature].needsInstantiations = new Set() + } for (const dep of behavior.uses) { const depname = dep.split('(', 1)[0] if (depname === 'self' || this._templateParam(depname)) { @@ -520,11 +751,42 @@ export default class PocomathInstance { * Reset an operation to require creation of typed-function, * and if it has no implementations so far, set them up. */ - _invalidate(name) { - if (this._invalid.has(name)) return + _invalidate(name, reason) { if (!(name in this._imps)) { this._imps[name] = {} + this._TFimps[name] = {} + this._metaTFimps[name] = {} } + if (reason) { + // Make sure no TF imps that depend on reason remain: + for (const [signature, behavior] of Object.entries(this._imps[name])) { + let invalidated = false + if (reason.charAt(0) === ':') { + const badType = reason.slice(1) + if (signature.includes(badType)) invalidated = true + } else { + for (const dep of behavior.uses) { + if (dep.includes(reason)) { + invalidated = true + break + } + } + } + if (invalidated) { + if (behavior.explicit) { + if (behavior.resolved) delete this._TFimps[signature] + behavior.resolved = false + } else { + for (const fullSig + of Object.values(behavior.hasInstantiations)) { + delete this._TFimps[fullSig] + } + behavior.hasInstantiations = {} + } + } + } + } + if (this._invalid.has(name)) return this._invalid.add(name) this._invalidateDependents(name) const self = this @@ -544,7 +806,7 @@ export default class PocomathInstance { _invalidateDependents(name) { if (name in this._affects) { for (const ancestor of this._affects[name]) { - this._invalidate(ancestor) + this._invalidate(ancestor, name) } } } @@ -558,13 +820,15 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } + const tf_imps = this._TFimps[name] + const meta_imps = this._metaTFimps[name] /* Collect the entries we know the types for */ const usableEntries = [] for (const entry of Object.entries(imps)) { let keep = true for (const type of typesOfSignature(entry[0])) { if (type in this.Types) continue - const baseType = type.split('<')[0] + const [baseType] = splitTemplate(type) if (baseType in this.Templates) continue keep = false break @@ -580,78 +844,109 @@ export default class PocomathInstance { * in the midst of being reassembled */ Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) - const tf_imps = {} for (const [rawSignature, behavior] of usableEntries) { - /* Check if it's an ordinary non-template signature */ - let explicit = true - for (const type of typesOfSignature(rawSignature)) { - for (const word of type.split(/[<>]/)) { - if (this._templateParam(word)) { - explicit = false - break - } + if (behavior.explicit) { + if (!(behavior.resolved)) { + this._addTFimplementation(tf_imps, rawSignature, behavior) + tf_imps[rawSignature]._pocoSignature = rawSignature + behavior.resolved = true } - } - if (explicit) { - this._addTFimplementation(tf_imps, rawSignature, behavior) continue } /* It's a template, have to instantiate */ - /* First, add the known instantiations, gathering all types needed */ - if (!('instantiations' in behavior)) { - behavior.instantiations = new Set() + /* 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[2] + 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 (ubType) behavior.needsInstantiations.add(ubType) let instantiationSet = new Set() - for (const instType of behavior.instantiations) { + const ubTypes = new Set() + if (!ubType) { + // Collect all upper-bound types for this signature + for (const othersig in imps) { + const thisUB = upperBounds.exec(othersig) + if (thisUB) ubTypes.add(thisUB[2]) + let basesig = othersig.replaceAll(templateCall, '') + if (basesig !== othersig) { + // A template + const testsig = substituteInSignature( + basesig, theTemplateParam, '') + if (testsig === basesig) { + // that is not also top-level + for (const templateType of typeListOfSignature(basesig)) { + if (templateType.slice(0,3) === '...') { + templateType = templateType.slice(3) + } + ubTypes.add(templateType) + } + } + } + } + } + for (const instType of behavior.needsInstantiations) { instantiationSet.add(instType) - for (const other of this._priorTypes[instType]) { - instantiationSet.add(other) + const otherTypes = + ubType ? this.subtypesOf(instType) : this._priorTypes[instType] + for (const other of otherTypes) { + if (!(this._atOrBelowSomeType(other, ubTypes))) { + instantiationSet.add(other) + } + } + } + + /* Prevent other existing signatures from blocking use of top-level + * templates via conversions: + */ + let baseSignature = rawSignature.replaceAll(templateCall, '') + /* Any remaining template params are top-level */ + const signature = substituteInSignature( + baseSignature, theTemplateParam, 'any') + const hasTopLevel = (signature !== baseSignature) + if (!ubType && hasTopLevel) { + for (const othersig in imps) { + let basesig = othersig.replaceAll(templateCall, '') + const testsig = substituteInSignature( + basesig, theTemplateParam, '') + if (testsig !== basesig) continue // a top-level template + for (let othertype of typeListOfSignature(othersig)) { + if (othertype.slice(0,3) === '...') { + othertype = othertype.slice(3) + } + if (this.Types[othertype] === anySpec) continue + const testType = substituteInSignature( + othertype, theTemplateParam, '') + let otherTypeCollection = [othertype] + if (testType !== othertype) { + const [base] = splitTemplate(othertype) + otherTypeCollection = this._instantiationsOf[base] + } + for (const possibility of otherTypeCollection) { + for (const convtype of this._priorTypes[possibility]) { + if (this.isSubtypeOf(convtype, possibility)) continue + if (!(this._atOrBelowSomeType(convtype, ubTypes))) { + instantiationSet.add(convtype) + } + } + } + } } } for (const instType of instantiationSet) { - if (!(instType in this.Types)) continue - if (this.Types[instType] === anySpec) continue - const signature = - substituteInSig(rawSignature, theTemplateParam, instType) - /* Don't override an explicit implementation: */ - if (signature in imps) continue - /* Don't go too deep */ - let maxdepth = 0 - for (const argType in typeListOfSignature(signature)) { - const depth = argType.split('<').length - if (depth > maxdepth) maxdepth = depth - } - if (maxdepth > this._maxDepthSeen + 1) continue - /* All right, go ahead and instantiate */ - const uses = new Set() - for (const dep of behavior.uses) { - if (this._templateParam(dep)) continue - uses.add(substituteInSig(dep, theTemplateParam, instType)) - } - const patch = (refs) => { - const innerRefs = {} - for (const dep of behavior.uses) { - if (this._templateParam(dep)) { - innerRefs[dep] = instType - } else { - const outerName = substituteInSig( - dep, theTemplateParam, instType) - innerRefs[dep] = refs[outerName] - } - } - return behavior.does(innerRefs) - } - this._addTFimplementation( - tf_imps, signature, {uses, does: patch}) + this._instantiateTemplateImplementation(name, rawSignature, instType) } /* Now add the catchall signature */ - let templateCall = `<${theTemplateParam}>` - /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = rawSignature.replaceAll(templateCall, '') - /* Any remaining template params are top-level */ - const signature = substituteInSig( - baseSignature, theTemplateParam, 'ground') + /* (Not needed if if it's a bounded template) */ + if (ubType) continue + if (behavior.resolved) continue /* The catchall signature has to detect the actual type of the call * and add the new instantiations. * First, prepare the type inference data: @@ -670,148 +965,131 @@ export default class PocomathInstance { throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } - /* And eliminate template parameters from the dependencies */ - const simplifiedUses = {} - for (const dep of behavior.uses) { - let [func, needsig] = dep.split(/[()]/) - if (needsig) { - const subsig = substituteInSig(needsig, theTemplateParam, '') - if (subsig === needsig) { - simplifiedUses[dep] = dep - } else { - simplifiedUses[dep] = func - } - } else { - simplifiedUses[dep] = dep - } - } + /* Now build the catchall implementation */ const self = this - const patch = (refs) => (...args) => { - /* We unbundle the rest arg if there is one */ - const regLength = args.length - 1 - if (restParam) { - const restArgs = args.pop() - args = args.concat(restArgs) - } - /* Now infer the type we actually should have been called for */ - let i = -1 - let j = -1 - /* collect the arg types */ - const argTypes = [] - for (const arg of args) { - ++j - // in case of rest parameter, reuse last parameter type: - if (i < inferences.length - 1) ++i - if (inferences[i]) { - const argType = inferences[i](arg) - if (!argType) { - throw TypeError( - `Type inference failed for argument ${j} of ${name}`) - } - if (argType === 'any') { - throw TypeError( - `In call to ${name}, incompatible template arguments: ` - // + args.map(a => JSON.stringify(a)).join(', ') - // unfortunately barfs on bigints. Need a better formatter - // wish we could just use the one that console.log uses; - // is that accessible somehow? - + args.map(a => a.toString()).join(', ') - + ' of types ' + argTypes.join(', ') + argType) - } - argTypes.push(argType) + /* For return type annotation, we may have to fix this to + propagate the return type. At the moment we are just bagging + */ + const patch = () => { + const patchFunc = (...tfBundledArgs) => { + /* We unbundle the rest arg if there is one */ + let args = Array.from(tfBundledArgs) + const regLength = args.length - 1 + if (restParam) { + const restArgs = args.pop() + args = args.concat(restArgs) } - } - if (argTypes.length === 0) { - throw TypeError('Type inference failed for' + name) - } - let usedConversions = false - let instantiateFor = self.joinTypes(argTypes) - if (instantiateFor === 'any') { - usedConversions = true - instantiateFor = self.joinTypes(argTypes, usedConversions) + /* Now infer the type we actually should have been called for */ + let i = -1 + let j = -1 + /* collect the arg types */ + const argTypes = [] + for (const arg of args) { + ++j + // in case of rest parameter, reuse last parameter type: + if (i < inferences.length - 1) ++i + if (inferences[i]) { + const argType = inferences[i](arg) + if (!argType) { + throw TypeError( + `Type inference failed for argument ${j} of ${name}`) + } + if (argType === 'any') { + throw TypeError( + `In call to ${name}, ` + + 'incompatible template arguments:' + // + args.map(a => JSON.stringify(a)).join(', ') + // unfortunately barfs on bigints. Need a better + // formatter. I wish we could just use the one that + // console.log uses; is that accessible somehow? + + args.map(a => a.toString()).join(', ') + + ' of types ' + argTypes.join(', ') + argType) + } + argTypes.push(argType) + } + } + if (argTypes.length === 0) { + throw TypeError('Type inference failed for' + name) + } + let usedConversions = false + let instantiateFor = self.joinTypes(argTypes) if (instantiateFor === 'any') { - throw TypeError( - `In call to ${name}, no type unifies arguments ` - + args.toString() + '; of types ' + argTypes.toString() - + '; note each consecutive pair must unify to a ' - + 'supertype of at least one of them') - } - } - const depth = instantiateFor.split('<').length - if (depth > self._maxDepthSeen) { - self._maxDepthSeen = depth - } - /* Generate the list of actual wanted types */ - const wantTypes = parTypes.map(type => substituteInSig( - type, theTemplateParam, instantiateFor)) - /* 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 - * by instantiateFor, and for all of instantiateFor's "prior types" - */ - for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { - // actually used the param and is a template - self._ensureTemplateTypes(parTypes[j], instantiateFor) - } - } - /* Transform the arguments if we used any conversions: */ - if (usedConversions) { - i = - 1 - for (j = 0; j < args.length; ++j) { - if (i < parTypes.length - 1) ++i - let wantType = parTypes[i] - if (wantType.slice(0,3) === '...') { - wantType = wantType.slice(3) - } - wantType = substituteInSig( - wantType, theTemplateParam, instantiateFor) - if (wantType !== parTypes[i]) { - args[j] = self._typed.convert(args[j], wantType) + usedConversions = true + instantiateFor = self.joinTypes(argTypes, usedConversions) + if (instantiateFor === 'any') { + throw TypeError( + `In call to ${name}, no type unifies arguments ` + + args.toString() + '; of types ' + argTypes.toString() + + '; note each consecutive pair must unify to a ' + + 'supertype of at least one of them') } } - } - /* Finally reassemble the rest args if there were any */ - if (restParam) { - const restArgs = args.slice(regLength) - args = args.slice(0,regLength) - args.push(restArgs) - } - /* Arrange that the desired instantiation will be there next - * time so we don't have to go through that again for this type - */ - refs[theTemplateParam] = instantiateFor - behavior.instantiations.add(instantiateFor) - self._invalidate(name) - // And update refs because we now know the type we're instantiating - // for: - const innerRefs = {} - for (const dep in simplifiedUses) { - const simplifiedDep = simplifiedUses[dep] - if (dep === simplifiedDep) { - innerRefs[dep] = refs[dep] - } else { - let [func, needsig] = dep.split(/[()]/) - if (self._typed.isTypedFunction(refs[simplifiedDep])) { - const subsig = substituteInSig( - needsig, theTemplateParam, instantiateFor) - let resname = simplifiedDep - if (resname == 'self') resname = name - innerRefs[dep] = self._pocoresolve( - resname, subsig, refs[simplifiedDep]) - } else { - innerRefs[dep] = refs[simplifiedDep] + const depth = instantiateFor.split('<').length + if (depth > self._maxDepthSeen) { + self._maxDepthSeen = depth + } + /* 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 by instantiateFor, and for all of + * instantiateFor's "prior types" + */ + for (j = 0; j < parTypes.length; ++j) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { + // actually used the param and is a template + self._ensureTemplateTypes(parTypes[j], instantiateFor) } } + + /* Request the desired instantiation: */ + // But possibly since this resolution was grabbed, the proper + // instantiation has been added (like if there are multiple + // uses in the implementation of another method. + if (!(behavior.needsInstantiations.has(instantiateFor))) { + behavior.needsInstantiations.add(instantiateFor) + self._invalidate(name) + } + const brandNewMe = self[name] + const whatToDo = self._typed.resolve(brandNewMe, args) + // We can access return type information here + // And in particular, if it might be a template, we should try to + // instantiate it: + const returnType = returnTypeOf(whatToDo.fn, wantSig, self) + for (const possibility of returnType.split('|')) { + const instantiated = self._maybeInstantiate(possibility) + if (instantiated) { + const [tempBase] = splitTemplate(instantiated) + self._invalidateDependents(':' + tempBase) + } + } + if (whatToDo === lastWhatToDo) { + throw new Error( + `Infinite recursion in resolving $name called on` + + args.map(x => x.toString()).join(',')) + } + lastWhatToDo = whatToDo + const retval = whatToDo.implementation(...args) + lastWhatToDo = null + return retval } - // Finally ready to make the call. - return behavior.does(innerRefs)(...args) + Object.defineProperty( + patchFunc, 'name', {value: `${name}(${signature})`}) + patchFunc._pocoSignature = rawSignature + return patchFunc } - // The actual uses value needs to be a set: - const outerUses = new Set(Object.values(simplifiedUses)) + Object.defineProperty( + patch, 'name', {value: `generate[${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. this._addTFimplementation( - tf_imps, signature, {uses: outerUses, does: patch}) + meta_imps, signature, {uses: new Set(), does: patch}) + behavior.resolved = true } this._correctPartialSelfRefs(name, tf_imps) // Make sure we have all of the needed (template) types; and if they @@ -820,35 +1098,140 @@ 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) { - badSigs.add(sig) - } + if (this._maybeInstantiate(type) === undefined) { + badSigs.add(sig) } } } for (const badSig of badSigs) { + const imp = tf_imps[badSig] delete tf_imps[badSig] + const fromBehavior = this._imps[name][imp._pocoSignature] + if (fromBehavior.explicit) { + fromBehavior.resolved = false + } else { + delete fromBehavior.hasInstantiations[imp._pocoInstance] + } } - const tf = this._typed(name, tf_imps) + let tf + if (Object.keys(tf_imps).length > 0) { + tf = this._typed(name, tf_imps) + tf.fromInstance = this + } + let metaTF + if (Object.keys(meta_imps).length > 0) { + metaTF = this._metaTyped(name, meta_imps) + metaTF.fromInstance = this + } + this._meta[name] = metaTF + + tf = tf || metaTF Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } + /* Takes a type and a set of types and returns true if the type + * is a subtype of some type in the set. + */ + _atOrBelowSomeType(type, typeSet) { + if (typeSet.has(type)) return true + let belowSome = false + for (const anUB of typeSet) { + if (anUB in this.Templates) { + if (type.slice(0, anUB.length) === anUB) { + belowSome = true + break + } + } else { + if (this.isSubtypeOf(type, anUB)) { + belowSome = true + break + } + } + } + return belowSome + } + + /* 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, arg] = splitTemplate(type) + return this.instantiateTemplate(base, arg) + } + + /* Generate and include a template instantiation for operation name + * for the template signature templateSignature instantiated for + * instanceType, returning the resulting implementation. + */ + _instantiateTemplateImplementation(name, templateSignature, instanceType) { + if (!(instanceType in this.Types)) return undefined + if (this.Types[instanceType] === anySpec) return undefined + const imps = this._imps[name] + const behavior = imps[templateSignature] + if (instanceType in behavior.hasInstantiations) return undefined + const signature = substituteInSignature( + templateSignature, theTemplateParam, instanceType) + /* Don't override an explicit implementation: */ + if (signature in imps) return undefined + /* Don't go too deep */ + let maxdepth = 0 + for (const argType in typeListOfSignature(signature)) { + const depth = argType.split('<').length + if (depth > maxdepth) maxdepth = depth + } + if (maxdepth > this._maxDepthSeen + 1) return undefined + /* All right, go ahead and instantiate */ + const uses = new Set() + for (const dep of behavior.uses) { + if (this._templateParam(dep)) continue + uses.add(substituteInSignature(dep, theTemplateParam, instanceType)) + } + const patch = (refs) => { + const innerRefs = {} + for (const dep of behavior.uses) { + if (this._templateParam(dep)) { + innerRefs[dep] = instanceType + } else { + const outerName = substituteInSignature( + dep, theTemplateParam, instanceType) + innerRefs[dep] = refs[outerName] + } + } + return behavior.does(innerRefs) + } + const tf_imps = this._TFimps[name] + this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + tf_imps[signature]._pocoSignature = templateSignature + tf_imps[signature]._pocoInstance = instanceType + behavior.hasInstantiations[instanceType] = signature + return tf_imps[signature] + } + /* 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 if (uses.length === 0) { - imps[signature] = does() + const implementation = does() + // could do something with return type information here + imps[signature] = implementation return } const refs = {} @@ -860,7 +1243,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: ' + @@ -869,13 +1252,23 @@ export default class PocomathInstance { } if (func === 'self') { if (needsig) { - if (full_self_referential) { - throw new SyntaxError( - 'typed-function does not support mixed full and ' - + 'partial self-reference') - } - if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { - part_self_references.push(needsig) + /* Maybe we can resolve the self reference without troubling + * typed-function: + */ + if (needsig in imps && typeof imps[needsig] == 'function') { + refs[dep] = imps[needsig] + } else { + if (full_self_referential) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + const needTypes = typesOfSignature(needsig) + const mergedTypes = Object.assign( + {}, this.Types, this.Templates) + if (subsetOfKeys(needTypes, mergedTypes)) { + part_self_references.push(needsig) + } } } else { if (part_self_references.length) { @@ -887,19 +1280,50 @@ export default class PocomathInstance { } } else { if (this[func] === 'limbo') { - /* We are in the midst of bundling func, so have to use - * an indirect reference to func. And given that, there's - * really no helpful way to extract a specific signature + /* We are in the midst of bundling func */ + let fallback = true + /* So the first thing we can do is try the tf_imps we are + * accumulating: */ - const self = this - refs[dep] = function () { // is this the most efficient? - return self[func].apply(this, arguments) + if (needsig) { + let typedUniverse + let tempTF + if (Object.keys(this._TFimps[func]).length > 0) { + typedUniverse = this._typed + tempTF = typedUniverse('dummy_' + func, this._TFimps[func]) + } else { + typedUniverse = this._metaTyped + tempTF = typedUniverse( + 'dummy_' + func, this._metaTFimps[func]) + } + let result = undefined + try { + result = typedUniverse.find(tempTF, needsig, {exact: true}) + } catch {} + if (result) { + refs[dep] = result + fallback = false + } + } + if (fallback) { + /* Either we need the whole function or the signature + * we need is not available yet, so we have to use + * an indirect reference to func. And given that, there's + * really no helpful way to extract a specific signature + */ + const self = this + const redirect = function () { // is this the most efficient? + return self[func].apply(this, arguments) + } + Object.defineProperty(redirect, 'name', {value: func}) + Object.defineProperty(redirect, 'fromInstance', {value: this}) + refs[dep] = redirect } } else { // can bundle up func, and grab its signature if need be let destination = this[func] - if (destination &&needsig) { - destination = this._pocoresolve(func, needsig) + if (destination && needsig) { + destination = this.resolve(func, needsig) } refs[dep] = destination } @@ -908,7 +1332,11 @@ export default class PocomathInstance { if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { refs.self = self - return does(refs) + const implementation = does(refs) + Object.defineProperty(implementation, 'name', {value: does.name}) + implementation.fromInstance = this + // What are we going to do with the return type info in here? + return implementation }) return } @@ -924,48 +1352,88 @@ export default class PocomathInstance { deferred: true, builtRefs: refs, sigDoes: does, + fromInstance: this, psr: part_self_references } return } - imps[signature] = does(refs) + const implementation = does(refs) + implementation.fromInstance = this + // could do something with return type information here? + imps[signature] = implementation } _correctPartialSelfRefs(name, imps) { for (const aSignature in imps) { if (!(imps[aSignature].deferred)) continue - const part_self_references = imps[aSignature].psr + const deferral = imps[aSignature] + const part_self_references = deferral.psr const corrected_self_references = [] + const remaining_self_references = [] + const refs = deferral.builtRefs for (const neededSig of part_self_references) { // Have to find a match for neededSig among the other signatures // of this function. That's a job for typed-function, but we will // try here: if (neededSig in imps) { // the easy case corrected_self_references.push(neededSig) + remaining_self_references.push(neededSig) continue } // No exact match, try to get one that matches with // subtypes since the whole conversion thing in typed-function // is too complicated to reproduce - const foundSig = this._findSubtypeImpl(name, imps, neededSig) + let foundSig = this._findSubtypeImpl(name, imps, neededSig) if (foundSig) { corrected_self_references.push(foundSig) + remaining_self_references.push(neededSig) } else { - throw new Error( - 'Implement inexact self-reference in typed-function for ' - + `${name}(${neededSig})`) + // Maybe it's a template instance we don't yet have + foundSig = this._findSubtypeImpl( + name, this._imps[name], neededSig) + if (foundSig) { + const match = this._pocoFindSignature(name, neededSig) + const neededTemplate = match.fn._pocoSignature + const neededInstance = whichSigInstance( + neededSig, neededTemplate) + const neededImplementation = + this._instantiateTemplateImplementation( + name, neededTemplate, neededInstance) + if (!neededImplementation) { + refs[`self(${neededSig})`] = match.implementation + } else { + if (typeof neededImplementation === 'function') { + refs[`self(${neededSig})`] = neededImplementation + } else { + corrected_self_references.push(neededSig) + remaining_self_references.push(neededSig) + } + } + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + `${name}(${neededSig})`) + } } } - const refs = imps[aSignature].builtRefs - const does = imps[aSignature].sigDoes - imps[aSignature] = this._typed.referTo( - ...corrected_self_references, (...impls) => { - for (let i = 0; i < part_self_references.length; ++i) { - refs[`self(${part_self_references[i]})`] = impls[i] + const does = deferral.sigDoes + if (remaining_self_references.length > 0) { + imps[aSignature] = this._typed.referTo( + ...corrected_self_references, (...impls) => { + for (let i = 0; i < remaining_self_references.length; ++i) { + refs[`self(${remaining_self_references[i]})`] = impls[i] + } + const implementation = does(refs) + // What will we do with the return type info in here? + return implementation } - return does(refs) - } - ) + ) + } else { + imps[aSignature] = does(refs) + } + imps[aSignature]._pocoSignature = deferral._pocoSignature + imps[aSignature]._pocoInstance = deferral._pocoInstance + imps[aSignature].fromInstance = deferral.fromInstance } } @@ -974,8 +1442,7 @@ export default class PocomathInstance { * in the instance. */ _ensureTemplateTypes(template, type) { - const base = template.split('<', 1)[0] - const arg = template.slice(base.length + 1, -1) + const [base, arg] = splitTemplate(template) if (!arg) { throw new Error( 'Implementation error in _ensureTemplateTypes', template, type) @@ -995,12 +1462,13 @@ 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). */ - instantiateTemplate(base, instantiator) { + instantiateTemplate = Returns('void', function(base, instantiator) { const depth = instantiator.split('<').length if (depth > this._maxDepthSeen ) { // don't bother with types much deeper thant we have seen @@ -1010,7 +1478,7 @@ export default class PocomathInstance { if (wantsType in this.Types) return false // OK, need to generate the type from the template // Set up refines, before, test, and from - const newTypeSpec = {refines: base} + const newTypeSpec = {} const maybeFrom = {} const template = this.Templates[base].spec if (!template) { @@ -1018,6 +1486,11 @@ export default class PocomathInstance { `Implementor error in instantiateTemplate(${base}, ${instantiator})`) } const instantiatorSpec = this.Types[instantiator] + if (instantiatorSpec.refines) { + this.instantiateTemplate(base, instantiatorSpec.refines) + // Assuming our templates are covariant, I guess + newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` + } let beforeTypes = [] if (instantiatorSpec.before) { beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) @@ -1025,36 +1498,35 @@ 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) { newTypeSpec.before = beforeTypes } - newTypeSpec.test = template.test(this._typeTests[instantiator]) + const templateTest = template.test(this._typeTests[instantiator]) + newTypeSpec.test = x => (template.base(x) && templateTest(x)) 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(/[<>]/)) { - if (word === templateFromParam) { - usesFromParam = true - break - } - } + const testSource = substituteInSignature( + instSource, templateFromParam, instantiator) + const usesFromParam = (testSource !== instSource) 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( - instSource, templateFromParam, subType) - maybeFrom[finalSource] = template.from[source](x => x) + if (testSource !== wantsType) { // subtypes handled with refines + // Assuming all templates are covariant here, I guess... + for (const subType of this._subtypes[instantiator]) { + const finalSource = substituteInSignature( + instSource, templateFromParam, subType) + maybeFrom[finalSource] = template.from[source](x => x) + } } } else { maybeFrom[instSource] = template.from[source] @@ -1066,8 +1538,9 @@ export default class PocomathInstance { newTypeSpec.from = maybeFrom } this.installType(wantsType, newTypeSpec) + this._instantiationsOf[base].add(wantsType) return wantsType - } + }) _findSubtypeImpl(name, imps, neededSig) { if (neededSig in imps) return neededSig @@ -1077,42 +1550,48 @@ export default class PocomathInstance { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true + let paramBound = 'any' 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}>`, '') + const adjustedOtherType = otherType.replaceAll(templateCall, '') if (adjustedOtherType !== otherType) { otherTypeList[k] = adjustedOtherType otherType = adjustedOtherType } 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)) { - allMatch = false - break - } - if (myType === otherType - || this._subtypes[otherType].has(myType)) { - continue - } + if (myType === otherType) continue if (otherType in this.Templates) { + const [myBase] = splitTemplate(myType) + if (myBase === otherType) continue if (this.instantiateTemplate(otherType, myType)) { let dummy dummy = this[name] // for side effects return this._findSubtypeImpl(name, this._imps[name], neededSig) } } + if (!(otherType in this.Types)) { + allMatch = false + break + } + if (this.isSubtypeOf(myType, otherType)) continue allMatch = false break } @@ -1124,28 +1603,96 @@ 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 - try { - result = this._typed.find(typedFunction, sig, {exact: true}) - } catch { + const haveTF = this._typed.isTypedFunction(typedFunction) + if (haveTF) { + // First try a direct match + let result + try { + result = this._typed.findSignature(typedFunction, sig, {exact: true}) + } catch { + } + if (result) return result + // Next, look ourselves but with subtypes: + 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) { + const implIndex = Math.min(i, implTypes.length - 1) + let implType = implTypes[implIndex] + if (implIndex < i) { + if (implType.slice(0,3) !== '...') { + // ran out of arguments in impl + allMatched = false + break + } + } + if (implType.slice(0,3) === '...') { + implType = implType.slice(3) + } + const hasMatch = implType.split('|').some( + t => (wantTypes[i] === t || this.isSubtypeOf(wantTypes[i], t))) + if (!hasMatch) { + allMatched = false + break + } + } + if (allMatched) return details + } } - if (result) return result + if (!(this._imps[name])) return undefined 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) { + if (haveTF) { + try { + return this._typed.findSignature(typedFunction, foundsig) + } catch { + } + } + try { + return this._metaTyped.findSignature(this._meta[name], foundsig) + } catch { + } + // We have an implementation but not a typed function. Do the best + // we can: + const foundImpl = this._imps[name][foundsig] + const needs = {} + for (const dep of foundImpl.uses) { + const [base, sig] = dep.split('()') + needs[dep] = this.resolve(base, sig) + } + const pseudoImpl = foundImpl.does(needs) + return {fn: pseudoImpl, implementation: pseudoImpl} + } + // Hmm, no luck. Make sure bundle is up-to-date and retry: + let result = undefined typedFunction = this[name] try { - result = this._typed.find(typedFunction, sig) + result = this._typed.findSignature(typedFunction, sig) } catch { } - if (result) return result + return result + } + + /* Returns a function that implements the operation with the given name + * when called with the given signature. The optional third argument is + * the typed function that provides the operation name, which can be + * passed in for efficiency if it is already available. + */ + resolve = Returns('function', function (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 - } + }) } diff --git a/src/core/Returns.mjs b/src/core/Returns.mjs new file mode 100644 index 0000000..211babd --- /dev/null +++ b/src/core/Returns.mjs @@ -0,0 +1,34 @@ +/* Annotate a function with its return type */ + +/* Unfortunately JavaScript is missing a way to cleanly clone a function + * object, see https://stackoverflow.com/questions/1833588 + */ + +const clonedFrom = Symbol('the original function this one was cloned from') +function cloneFunction(fn) { + const behavior = fn[clonedFrom] || fn // don't nest clones + const theClone = function () { return behavior.apply(this, arguments) } + Object.assign(theClone, fn) + theClone[clonedFrom] = body + Object.defineProperty( + theClone, 'name', {value: fn.name, configurable: true }) + return theClone +} + +export function Returns(type, fn) { + if ('returns' in fn) fn = cloneFunction(fn) + fn.returns = type + return fn +} + +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 Returns + diff --git a/src/core/utils.mjs b/src/core/utils.mjs index db164dd..9ae9aab 100644 --- a/src/core/utils.mjs +++ b/src/core/utils.mjs @@ -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()) } diff --git a/src/generic/Types/adapted.mjs b/src/generic/Types/adapted.mjs index ba20889..6c54fc5 100644 --- a/src/generic/Types/adapted.mjs +++ b/src/generic/Types/adapted.mjs @@ -1,4 +1,6 @@ import PocomathInstance from '../../core/PocomathInstance.mjs' +import Returns from '../../core/Returns.mjs' + /* creates a PocomathInstance incorporating a new numeric type encapsulated * as a class. (This instance can the be `install()ed` in another to add the * type so created.) @@ -22,15 +24,15 @@ export default function adapted(name, Thing, overrides) { // first a creator function, with name depending on the name of the thing: const creatorName = overrides.creatorName || name.toLowerCase() const creator = overrides[creatorName] - ? overrides[creatorName]('') + ? overrides[creatorName][''] : Thing[creatorName] ? (Thing[creatorName]) : ((...args) => new Thing(...args)) const defaultCreatorImps = { - '': () => () => creator(), - '...any': () => args => creator(...args) + '': () => Returns(name, () => creator()), + '...any': () => Returns(name, args => creator(...args)) } - defaultCreatorImps[name] = () => x => x // x.clone(x)? + defaultCreatorImps[name] = () => Returns(name, x => x) // x.clone(x)? operations[creatorName] = overrides[creatorName] || defaultCreatorImps // We make the default instance, just as a place to check for methods @@ -47,34 +49,35 @@ export default function adapted(name, Thing, overrides) { negate: 'neg' } const binaryOps = { - add: 'add', - compare: 'compare', - divide: 'div', - equalTT: 'equals', - gcd: 'gcd', - lcm: 'lcm', - mod: 'mod', - multiply: 'mul', - subtract: 'sub' + add: ['add', name], + compare: ['compare', name], + divide: ['div', name], + equalTT: ['equals', 'boolean'], + gcd: ['gcd', name], + lcm: ['lcm', name], + mod: ['mod', name], + multiply: ['mul', name], + subtract: ['sub', name] } for (const [mathname, standardname] of Object.entries(unaryOps)) { if (standardname in instance) { operations[mathname] = {} - operations[mathname][name] = () => t => t[standardname]() + operations[mathname][name] = () => Returns(name, t => t[standardname]()) } } operations.zero = {} - operations.zero[name] = () => t => creator() + operations.zero[name] = () => Returns(name, t => creator()) operations.one = {} - operations.one[name] = () => t => creator(1) + operations.one[name] = () => Returns(name, t => creator(1)) operations.conjugate = {} - operations.conjugate[name] = () => t => t // or t.clone() ?? + operations.conjugate[name] = () => Returns(name, t => t) // or t.clone() ?? const binarySignature = `${name},${name}` - for (const [mathname, standardname] of Object.entries(binaryOps)) { - if (standardname in instance) { + for (const [mathname, spec] of Object.entries(binaryOps)) { + if (spec[0] in instance) { operations[mathname] = {} - operations[mathname][binarySignature] = () => (t,u) => t[standardname](u) + operations[mathname][binarySignature] = () => Returns( + spec[1], (t,u) => t[spec[0]](u)) } } if ('operations' in overrides) { diff --git a/src/generic/abs.mjs b/src/generic/abs.mjs index 84ebc31..ff20ee8 100644 --- a/src/generic/abs.mjs +++ b/src/generic/abs.mjs @@ -1,7 +1,9 @@ +import Returns from '../core/Returns.mjs' export const abs = { T: ({ + T, 'smaller(T,T)': lt, 'negate(T)': neg, 'zero(T)': zr - }) => t => (smaller(t, zr(t)) ? neg(t) : t) + }) => Returns(T, t => (smaller(t, zr(t)) ? neg(t) : t)) } diff --git a/src/generic/absquare.mjs b/src/generic/absquare.mjs index 26d6717..052131d 100644 --- a/src/generic/absquare.mjs +++ b/src/generic/absquare.mjs @@ -1,6 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const absquare = { T: ({ + T, 'square(T)': sq, 'abs(T)': abval - }) => t => sq(abval(t)) + }) => Returns(T, t => sq(abval(t))) } diff --git a/src/generic/all.mjs b/src/generic/all.mjs index 7132944..45bd9d0 100644 --- a/src/generic/all.mjs +++ b/src/generic/all.mjs @@ -1,5 +1,6 @@ import {adapted} from './Types/adapted.mjs' import Fraction from 'fraction.js/bigfraction.js' +import Returns from '../core/Returns.mjs' export * from './arithmetic.mjs' export * from './relational.mjs' @@ -8,15 +9,18 @@ export const fraction = adapted('Fraction', Fraction, { before: ['Complex'], from: {number: n => new Fraction(n)}, operations: { - compare: {'Fraction,Fraction': () => (f,g) => new Fraction(f.compare(g))}, + compare: { + 'Fraction,Fraction': () => Returns( + 'Fraction', (f,g) => new Fraction(f.compare(g))) + }, mod: { - 'Fraction,Fraction': () => (n,d) => { + 'Fraction,Fraction': () => Returns('Fraction', (n,d) => { // patch for "mathematician's modulus" // OK to use full public API of Fraction here const fmod = n.mod(d) if (fmod.s === -1n) return fmod.add(d.abs()) return fmod - } + }) } } }) diff --git a/src/generic/divide.mjs b/src/generic/divide.mjs index 1aee89b..ab1e893 100644 --- a/src/generic/divide.mjs +++ b/src/generic/divide.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export const divide = { 'T,T': ({ + T, 'multiply(T,T)': multT, 'invert(T)': invT - }) => (x, y) => multT(x, invT(y)) + }) => Returns(T, (x, y) => multT(x, invT(y))) } diff --git a/src/generic/gcdType.mjs b/src/generic/gcdType.mjs index 1ca16ab..ee86b50 100644 --- a/src/generic/gcdType.mjs +++ b/src/generic/gcdType.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + /* Note we do not use a template here so that we can explicitly control * which types this is instantiated for, namely the "integer" types, and * not simply allow Pocomath to generate instances for any type it encounters. @@ -7,14 +9,14 @@ export default function(type) { const producer = refs => { const modder = refs[`mod(${type},${type})`] const zeroTester = refs[`isZero(${type})`] - return (a,b) => { + return Returns(type, (a,b) => { while (!zeroTester(b)) { const r = modder(a,b) a = b b = r } return a - } + }) } const retval = {} retval[`${type},${type}`] = producer diff --git a/src/generic/identity.mjs b/src/generic/identity.mjs index 2422d2f..fb3853e 100644 --- a/src/generic/identity.mjs +++ b/src/generic/identity.mjs @@ -1,3 +1,11 @@ -export function identity(x) { - return x +import Returns from '../core/Returns.mjs' + +export function identityType(type) { + return () => Returns(type, x => x) } + +export function identitySubTypes(type) { + return ({T}) => Returns(T, x => x) +} + +export const identity = {T: ({T}) => Returns(T, x => x)} diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs index 04e78b5..26bfbf8 100644 --- a/src/generic/lcm.mjs +++ b/src/generic/lcm.mjs @@ -1,10 +1,12 @@ +import Returns from '../core/Returns.mjs' import {reducingOperation} from './reducingOperation.mjs' export const lcm = { 'T,T': ({ + T, 'multiply(T,T)': multT, 'quotient(T,T)': quotT, 'gcd(T,T)': gcdT - }) => (a,b) => multT(quotT(a, gcdT(a,b)), b) + }) => Returns(T, (a,b) => multT(quotT(a, gcdT(a,b)), b)) } Object.assign(lcm, reducingOperation) diff --git a/src/generic/mean.mjs b/src/generic/mean.mjs index d12c21b..58cbc19 100644 --- a/src/generic/mean.mjs +++ b/src/generic/mean.mjs @@ -1,3 +1,8 @@ +import Returns from '../core/Returns.mjs' export const mean = { - '...any': ({add, divide}) => args => divide(add(...args), args.length) + '...T': ({ + T, + add, + 'divide(T,NumInt)': div + }) => Returns(T, args => div(add(...args), args.length)) } diff --git a/src/generic/mod.mjs b/src/generic/mod.mjs index 84af4e6..e1b5ec6 100644 --- a/src/generic/mod.mjs +++ b/src/generic/mod.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export const mod = { 'T,T': ({ + T, 'subtract(T,T)': subT, 'multiply(T,T)': multT, 'quotient(T,T)': quotT - }) => (a,m) => subT(a, multT(m, quotT(a,m))) + }) => Returns(T, (a,m) => subT(a, multT(m, quotT(a,m)))) } diff --git a/src/generic/quotient.mjs b/src/generic/quotient.mjs index 54e000a..521bd2a 100644 --- a/src/generic/quotient.mjs +++ b/src/generic/quotient.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const quotient = { - 'T,T': ({'floor(T)': flr, 'divide(T,T)':div}) => (n,d) => flr(div(n,d)) + 'T,T': ({ + T, + 'floor(T)': flr, + 'divide(T,T)': div + }) => Returns(T, (n,d) => flr(div(n,d))) } diff --git a/src/generic/reducingOperation.mjs b/src/generic/reducingOperation.mjs index e29baf1..3c256f2 100644 --- a/src/generic/reducingOperation.mjs +++ b/src/generic/reducingOperation.mjs @@ -1,13 +1,16 @@ +import Returns from '../core/Returns.mjs' export * from './Types/generic.mjs' export const reducingOperation = { - 'undefined': () => u => u, - 'undefined,...any': () => (u, rest) => u, - 'any,undefined': () => (x, u) => u, - 'undefined,undefined': () => (u,v) => u, - any: () => x => x, + 'undefined': () => Returns('undefined', u => u), + 'undefined,...any': () => Returns('undefined', (u, rest) => u), + 'any,undefined': () => Returns('undefined', (x, u) => u), + 'undefined,undefined': () => Returns('undefined', (u,v) => u), + T: ({T}) => Returns(T, x => x), + // Unfortunately the type language of Pocomath is not (yet?) expressive + // enough to properly type the full reduction signature here: 'any,any,...any': ({ self - }) => (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a) + }) => Returns('any', (a,b,rest) => [b, ...rest].reduce((x,y) => self(x,y), a)) } diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index a1639a1..72de34c 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -1,34 +1,45 @@ +import Returns from '../core/Returns.mjs' + export const compare = { - 'undefined,undefined': () => () => 0 + 'undefined,undefined': () => Returns('NumInt', () => 0) } export const isZero = { - 'undefined': () => u => u === 0, - T: ({'equal(T,T)': eq, 'zero(T)': zr}) => t => eq(t, zr(t)) + 'undefined': () => Returns('boolean', u => u === 0), + T: ({ + T, + 'equal(T,T)': eq, + 'zero(T)': zr + }) => Returns('boolean', t => eq(t, zr(t))) } export const equal = { - 'any,any': ({equalTT, joinTypes, Templates, typeOf}) => (x,y) => { + 'any,any': ({ + equalTT, + joinTypes, + Templates, + typeOf + }) => Returns('boolean', (x,y) => { const resultant = joinTypes([typeOf(x), typeOf(y)], 'convert') if (resultant === 'any' || resultant in Templates) { return false } return equalTT(x,y) - } + }) } export const equalTT = { 'T,T': ({ 'compare(T,T)': cmp, 'isZero(T)': isZ - }) => (x,y) => isZ(cmp(x,y)), + }) => Returns('boolean', (x,y) => isZ(cmp(x,y))) // If templates were native to typed-function, we should be able to // do something like: // 'any,any': () => () => false // should only be hit for different types } export const unequal = { - 'any,any': ({equal}) => (x,y) => !(equal(x,y)) + 'any,any': ({equal}) => Returns('boolean', (x,y) => !(equal(x,y))) } export const larger = { @@ -36,7 +47,7 @@ export const larger = { 'compare(T,T)': cmp, 'one(T)' : uno, 'equalTT(T,T)' : eq - }) => (x,y) => eq(cmp(x,y), uno(y)) + }) => Returns('boolean', (x,y) => eq(cmp(x,y), uno(y))) } export const largerEq = { @@ -45,10 +56,10 @@ export const largerEq = { 'one(T)' : uno, 'isZero(T)' : isZ, 'equalTT(T,T)': eq - }) => (x,y) => { + }) => Returns('boolean', (x,y) => { const c = cmp(x,y) return isZ(c) || eq(c, uno(y)) - } + }) } export const smaller = { @@ -57,10 +68,10 @@ export const smaller = { 'one(T)' : uno, 'isZero(T)' : isZ, unequal - }) => (x,y) => { + }) => Returns('boolean', (x,y) => { const c = cmp(x,y) return !isZ(c) && unequal(c, uno(y)) - } + }) } export const smallerEq = { @@ -68,5 +79,5 @@ export const smallerEq = { 'compare(T,T)': cmp, 'one(T)' : uno, unequal - }) => (x,y) => unequal(cmp(x,y), uno(y)) + }) => Returns('boolean', (x,y) => unequal(cmp(x,y), uno(y))) } diff --git a/src/generic/roundquotient.mjs b/src/generic/roundquotient.mjs index 5346882..9c2ba2b 100644 --- a/src/generic/roundquotient.mjs +++ b/src/generic/roundquotient.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const roundquotient = { - 'T,T': ({'round(T)': rnd, 'divide(T,T)':div}) => (n,d) => rnd(div(n,d)) + 'T,T': ({ + T, + 'round(T)': rnd, + 'divide(T,T)':div + }) => Returns(T, (n,d) => rnd(div(n,d))) } diff --git a/src/generic/sign.mjs b/src/generic/sign.mjs index 769e2c9..cec73cd 100644 --- a/src/generic/sign.mjs +++ b/src/generic/sign.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const sign = { - T: ({'compare(T,T)': cmp, 'zero(T)': Z}) => x => cmp(x, Z(x)) + T: ({ + T, + 'compare(T,T)': cmp, + 'zero(T)': Z + }) => Returns(T, x => cmp(x, Z(x))) } diff --git a/src/generic/sqrt.mjs b/src/generic/sqrt.mjs index 21aa1d5..faea759 100644 --- a/src/generic/sqrt.mjs +++ b/src/generic/sqrt.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/generic.mjs' -export const sqrt = {undefined: () => () => undefined} +export const sqrt = {undefined: () => Returns('undefined', () => undefined)} diff --git a/src/generic/square.mjs b/src/generic/square.mjs index 53fd6c2..2619c29 100644 --- a/src/generic/square.mjs +++ b/src/generic/square.mjs @@ -1,3 +1,6 @@ +import {Returns, returnTypeOf} from '../core/Returns.mjs' + export const square = { - T: ({'multiply(T,T)': multT}) => x => multT(x,x) + T: ({'multiply(T,T)': multT}) => Returns( + returnTypeOf(multT), x => multT(x,x)) } diff --git a/src/generic/subtract.mjs b/src/generic/subtract.mjs index b048d0c..35dab22 100644 --- a/src/generic/subtract.mjs +++ b/src/generic/subtract.mjs @@ -1,3 +1,9 @@ +import Returns from '../core/Returns.mjs' + export const subtract = { - 'T,T': ({'add(T,T)': addT, 'negate(T)': negT}) => (x,y) => addT(x, negT(y)) + 'T,T': ({ + T, + 'add(T,T)': addT, + 'negate(T)': negT + }) => Returns(T, (x,y) => addT(x, negT(y))) } diff --git a/src/number/abs.mjs b/src/number/abs.mjs index 66ede16..80b45d8 100644 --- a/src/number/abs.mjs +++ b/src/number/abs.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const abs = {number: () => n => Math.abs(n)} +export const abs = {'T:number': ({T}) => Returns(T, n => Math.abs(n))} diff --git a/src/number/absquare.mjs b/src/number/absquare.mjs index d6ab55a..31a417d 100644 --- a/src/number/absquare.mjs +++ b/src/number/absquare.mjs @@ -1,6 +1,7 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' /* Absolute value squared */ export const absquare = { - number: ({'square(number)': sqn}) => n => sqn(n) + 'T:number': ({T, 'square(T)': sqn}) => Returns(T, n => sqn(n)) } diff --git a/src/number/add.mjs b/src/number/add.mjs index 7d79637..41791cf 100644 --- a/src/number/add.mjs +++ b/src/number/add.mjs @@ -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) +} diff --git a/src/number/compare.mjs b/src/number/compare.mjs index 4dc865b..c4b1c26 100644 --- a/src/number/compare.mjs +++ b/src/number/compare.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + /* Lifted from mathjs/src/utils/number.js */ /** * Minimum number added to one that makes the result different than one @@ -48,5 +50,6 @@ function nearlyEqual (x, y, epsilon) { export const compare = { 'number,number': ({ config - }) => (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1) + }) => Returns( + 'NumInt', (x,y) => nearlyEqual(x, y, config.epsilon) ? 0 : (x > y ? 1 : -1)) } diff --git a/src/number/invert.mjs b/src/number/invert.mjs index 4eabe2f..780ad72 100644 --- a/src/number/invert.mjs +++ b/src/number/invert.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const invert = {number: () => n => 1/n} +export const invert = {number: () => Returns('number', n => 1/n)} diff --git a/src/number/isZero.mjs b/src/number/isZero.mjs index c15549e..0209daa 100644 --- a/src/number/isZero.mjs +++ b/src/number/isZero.mjs @@ -1,6 +1,6 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const isZero = { - number: () => n => n === 0, - NumInt: () => n => n === 0 // necessary because of generic template + 'T:number': () => Returns('boolean', n => n === 0) } diff --git a/src/number/multiply.mjs b/src/number/multiply.mjs index 80573d1..5951f22 100644 --- a/src/number/multiply.mjs +++ b/src/number/multiply.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const multiply = {'number,number': () => (m,n) => m*n} +export const multiply = {'T:number,T': ({T}) => Returns(T, (m,n) => m*n)} diff --git a/src/number/native.mjs b/src/number/native.mjs index 6746408..fcebecc 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -1,5 +1,5 @@ import gcdType from '../generic/gcdType.mjs' -import {identity} from '../generic/identity.mjs' +import {identitySubTypes} from '../generic/identity.mjs' export * from './Types/number.mjs' @@ -7,7 +7,7 @@ export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' -export const conjugate = {number: () => identity} +export const conjugate = {'T:number': identitySubTypes('number')} export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/number/negate.mjs b/src/number/negate.mjs index 82e27d0..f2336c7 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,3 +1,6 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' -export const negate = {number: () => n => -n} +export const negate = { + 'T:number': ({T}) => Returns(T, n => -n) +} diff --git a/src/number/one.mjs b/src/number/one.mjs index 5726468..e38d0dc 100644 --- a/src/number/one.mjs +++ b/src/number/one.mjs @@ -1,3 +1,5 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -export const one = {number: () => () => 1} +export const one = {number: () => Returns('NumInt', () => 1)} diff --git a/src/number/quotient.mjs b/src/number/quotient.mjs index e8ed83a..b307709 100644 --- a/src/number/quotient.mjs +++ b/src/number/quotient.mjs @@ -1,15 +1,10 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' -const intquotient = () => (n,d) => { - if (d === 0) return d - return Math.floor(n/d) -} - export const quotient = { - // Hmmm, seem to need all of these because of the generic template version - // Should be a way around that - 'NumInt,NumInt': intquotient, - 'NumInt,number': intquotient, - 'number,NumInt': intquotient, - 'number,number': intquotient + 'T:number,T': () => Returns('NumInt', (n,d) => { + if (d === 0) return d + return Math.floor(n/d) + }) } diff --git a/src/number/roundquotient.mjs b/src/number/roundquotient.mjs index 401d499..8c4c519 100644 --- a/src/number/roundquotient.mjs +++ b/src/number/roundquotient.mjs @@ -1,8 +1,10 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/number.mjs' export const roundquotient = { - 'number,number': () => (n,d) => { + 'number,number': () => Returns('NumInt', (n,d) => { if (d === 0) return d return Math.round(n/d) - } + }) } diff --git a/src/number/sqrt.mjs b/src/number/sqrt.mjs index 3017e82..2e5e734 100644 --- a/src/number/sqrt.mjs +++ b/src/number/sqrt.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const sqrt = { @@ -5,13 +6,13 @@ export const sqrt = { config, 'complex(number,number)': cplx, 'negate(number)': neg}) => { - if (config.predictable || !cplx) { - return n => isNaN(n) ? NaN : Math.sqrt(n) + if (config.predictable || !cplx) { + return Returns('number', n => isNaN(n) ? NaN : Math.sqrt(n)) + } + return Returns('number|Complex', n => { + if (isNaN(n)) return NaN + if (n >= 0) return Math.sqrt(n) + return cplx(0, Math.sqrt(neg(n))) + }) } - return n => { - if (isNaN(n)) return NaN - if (n >= 0) return Math.sqrt(n) - return cplx(0, Math.sqrt(neg(n))) - } - } } diff --git a/src/number/zero.mjs b/src/number/zero.mjs index 40ac2fb..5e3e3a3 100644 --- a/src/number/zero.mjs +++ b/src/number/zero.mjs @@ -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)} diff --git a/src/ops/choose.mjs b/src/ops/choose.mjs index c285dc7..dd22dc2 100644 --- a/src/ops/choose.mjs +++ b/src/ops/choose.mjs @@ -1,11 +1,13 @@ -/* Note this is not a good algorithm for computing binomial coefficients, +import Returns from '../core/Returns.mjs' + +/* Note this is _not_ a good algorithm for computing binomial coefficients, * it's just for demonstration purposes */ export const choose = { - 'NumInt,NumInt': ({factorial}) => (n,k) => Number( - factorial(n) / (factorial(k)*factorial(n-k))), + 'NumInt,NumInt': ({factorial}) => Returns( + 'NumInt', (n,k) => Number(factorial(n) / (factorial(k)*factorial(n-k)))), 'bigint,bigint': ({ factorial - }) => (n,k) => factorial(n) / (factorial(k)*factorial(n-k)) + }) => Returns('bigint', (n,k) => factorial(n) / (factorial(k)*factorial(n-k))) } diff --git a/src/ops/factorial.mjs b/src/ops/factorial.mjs index bb07047..b1154f3 100644 --- a/src/ops/factorial.mjs +++ b/src/ops/factorial.mjs @@ -1,8 +1,15 @@ -export function factorial(n) { +import Returns from '../core/Returns.mjs' + +/* Plain functions are OK, too, and they can be decorated with a return type + * just like an implementation. + */ +const factorial = Returns('bigint', function factorial(n) { n = BigInt(n) let prod = 1n for (let i = n; i > 1n; --i) { prod *= i } return prod -} +}) + +export {factorial} diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 5fdb9e7..3754dcb 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -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,26 +6,28 @@ 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': () => 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': () => Returns('Complex', 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': Complex.promoteUnary['Complex'], // OK to include a type totally not in Pocomath yet, it'll never be // activated. - // Fraction: ({quotient}) => f => quotient(f.n, f.d), // oops have that now 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()) } diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index d76d147..4ff6685 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -1,25 +1,24 @@ /* A template type representing a homeogeneous tuple of elements */ import PocomathInstance from '../../core/PocomathInstance.mjs' +import {Returns, returnTypeOf} from '../../core/Returns.mjs' const Tuple = new PocomathInstance('Tuple') -// First a base type that will generally not be used directly -Tuple.installType('Tuple', { - test: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts) -}) -// Now the template type that is the primary use of this + Tuple.installType('Tuple', { - // We are assuming that any 'Type' refines 'Type', so this is - // not necessary: - // refines: 'Tuple', - // But we need there to be a way to determine the type of a tuple: - infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), - // For the test, we can assume that t is already a base tuple, - // and we get the test for T as an input and we have to return - // the test for Tuple + // A test that "defines" the "base type", which is not really a type + // (only fully instantiated types are added to the universe) + base: t => t && typeof t === 'object' && 'elts' in t && Array.isArray(t.elts), + // The template portion of the test; it takes the test for T as + // input and returns the test for an entity _that already passes + // the base test_ to be a Tuple: test: testT => t => t.elts.every(testT), - // These are only invoked for types U such that there is already - // a conversion from U to T, and that conversion is passed as an input - // and we have to return the conversion to Tuple: + // And we need there to be a way to determine the (instantiation) + // type of an tuple (that has already passed the base test): + infer: ({typeOf, joinTypes}) => t => joinTypes(t.elts.map(typeOf)), + // Conversions. Parametrized conversions are only invoked for types + // U such that there is already a conversion from U to T, and that + // conversion is passed as an input, and we have to return the conversion + // function from the indicated template in terms of U to Tuple: from: { 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), // Here since there is no U it's a straight conversion: @@ -35,50 +34,66 @@ Tuple.promoteUnary = { 'Tuple': ({ 'self(T)': me, tuple - }) => t => tuple(...(t.elts.map(x => me(x)))) // NOTE: this must use - // the inner arrow function to drop additional arguments that Array.map - // supplies, as otherwise the wrong signature of `me` might be used. + }) => { + const compType = me.fromInstance.joinTypes( + returnTypeOf(me).split('|'), 'convert') + return Returns( + `Tuple<${compType}>`, t => tuple(...(t.elts.map(x => me(x))))) + } } Tuple.promoteBinaryUnary = { - 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => (s,t) => { - let i = -1 - let result = [] - while (true) { - i += 1 - if (i < s.elts.length) { - if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i])) - else result.push(meU(s.elts[i])) - continue + 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU, tuple}) => { + const compTypes = returnTypeOf(meB).split('|').concat( + returnTypeOf(meU).split('|')) + const compType = meU.fromInstance.joinTypes(compTypes, 'convert') + return Returns(`Tuple<${compType}>`, (s,t) => { + let i = -1 + let result = [] + while (true) { + i += 1 + if (i < s.elts.length) { + if (i < t.elts.length) result.push(meB(s.elts[i], t.elts[i])) + else result.push(meU(s.elts[i])) + continue + } + if (i < t.elts.length) result.push(meU(t.elts[i])) + else break } - if (i < t.elts.length) result.push(meU(t.elts[i])) - else break - } - return tuple(...result) + return tuple(...result) + }) } } Tuple.promoteBinary = { - 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { - const lim = Math.max(s.elts.length, t.elts.length) - const result = [] - for (let i = 0; i < lim; ++i) { - result.push(meB(s.elts[i], t.elts[i])) - } - return tuple(...result) + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => { + const compType = meB.fromInstance.joinTypes( + returnTypeOf(meB).split('|')) + return Returns(`Tuple<${compType}>`, (s,t) => { + const lim = Math.max(s.elts.length, t.elts.length) + const result = [] + for (let i = 0; i < lim; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + }) } } Tuple.promoteBinaryStrict = { - 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => (s,t) => { - if (s.elts.length !== t.elts.length) { - throw new RangeError('Tuple length mismatch') // get name of self ?? - } - const result = [] - for (let i = 0; i < s.elts.length; ++i) { - result.push(meB(s.elts[i], t.elts[i])) - } - return tuple(...result) + 'Tuple,Tuple': ({'self(T,T)': meB, tuple}) => { + const compType = meB.fromInstance.joinTypes( + returnTypeOf(meB).split('|')) + return Returns(`Tuple<${compType}>`, (s,t) => { + if (s.elts.length !== t.elts.length) { + throw new RangeError('Tuple length mismatch') // get name of self ?? + } + const result = [] + for (let i = 0; i < s.elts.length; ++i) { + result.push(meB(s.elts[i], t.elts[i])) + } + return tuple(...result) + }) } } diff --git a/src/tuple/equalTT.mjs b/src/tuple/equalTT.mjs index 1606410..557ee2d 100644 --- a/src/tuple/equalTT.mjs +++ b/src/tuple/equalTT.mjs @@ -1,11 +1,16 @@ +import Returns from '../core/Returns.mjs' + export * from './Types/Tuple.mjs' export const equalTT = { - 'Tuple,Tuple': ({'self(T,T)': me, 'length(Tuple)': len}) => (s,t) => { + 'Tuple,Tuple': ({ + 'self(T,T)': me, + 'length(Tuple)': len + }) => Returns('boolean', (s,t) => { if (len(s) !== len(t)) return false for (let i = 0; i < len(s); ++i) { if (!me(s.elts[i], t.elts[i])) return false } return true - } + }) } diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs index 9375277..a48e92a 100644 --- a/src/tuple/isZero.mjs +++ b/src/tuple/isZero.mjs @@ -1,7 +1,10 @@ +import Returns from '../core/Returns.mjs' + export {Tuple} from './Types/Tuple.mjs' export const isZero = { - 'Tuple': ({'self(T)': me}) => t => t.elts.every(e => me(e)) + 'Tuple': ({'self(T)': me}) => Returns( + 'boolean', t => t.elts.every(e => me(e))) // Note we can't just say `every(me)` above since every invokes its // callback with more arguments, which then violates typed-function's // signature for `me` diff --git a/src/tuple/length.mjs b/src/tuple/length.mjs index f3e8f2d..4df2c74 100644 --- a/src/tuple/length.mjs +++ b/src/tuple/length.mjs @@ -1,3 +1,4 @@ +import Returns from '../core/Returns.mjs' export {Tuple} from './Types/Tuple.mjs' -export const length = {Tuple: () => t => t.elts.length} +export const length = {'Tuple': () => Returns('NumInt', t => t.elts.length)} diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs index 893b54d..9cd0c65 100644 --- a/src/tuple/tuple.mjs +++ b/src/tuple/tuple.mjs @@ -1,6 +1,10 @@ +import Returns from '../core/Returns.mjs' + export {Tuple} from './Types/Tuple.mjs' /* The purpose of the template argument is to ensure that all of the args * are convertible to the same type. */ -export const tuple = {'...T': () => args => ({elts: args})} +export const tuple = { + '...T': ({T}) => Returns(`Tuple<${T}>`, args => ({elts: args})) +} diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 38df18b..c4a5c28 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -20,9 +20,56 @@ describe('The default full pocomath instance "math"', () => { assert.strictEqual(math.typeOf({re: 6.28, im: 2.72}), 'Complex') }) + 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'), 'Complex') + 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,NumInt'), 'Complex') + // The following is not actually what we want, but the Pocomath type + // language isn't powerful enough at this point to capture the true + // return type: + assert.strictEqual( + math.returnTypeOf('add', 'number,NumInt,Complex'), 'any') + assert.strictEqual( + math.returnTypeOf('chain', 'bigint'), 'Chain') + assert.strictEqual( + math.returnTypeOf('returnTypeOf', 'string,string'), 'string') + assert.strictEqual( + math.returnTypeOf('conjugate', 'bigint'), 'bigint') + assert.strictEqual( + math.returnTypeOf('gcd', 'bigint,bigint'), 'bigint') + math.identity(math.fraction(3,5)) // TODO: ditto + assert.strictEqual(math.returnTypeOf('identity', 'Fraction'), 'Fraction') + assert.strictEqual( + math.returnTypeOf('quotient', 'bigint,bigint'), 'bigint') + math.abs(math.complex(2,1)) //TODO: ditto + assert.strictEqual( + math.returnTypeOf('abs','Complex'), 'number') + math.multiply(math.quaternion(1,1,1,1), math.quaternion(1,-1,1,-1)) // dit + const quatType = math.returnTypeOf( + 'quaternion', 'NumInt,NumInt,NumInt,NumInt') + assert.strictEqual(quatType, 'Complex>') + assert.strictEqual( + math.returnTypeOf('multiply', quatType + ',' + quatType), quatType) + assert.strictEqual(math.returnTypeOf('isZero', 'NumInt'), 'boolean') + assert.strictEqual( + math.returnTypeOf('roundquotient', 'NumInt,number'), 'NumInt') + assert.strictEqual( + math.returnTypeOf('factorial', 'NumInt'), 'bigint') + }) + 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', () => { diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 531a28d..413503c 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -39,8 +39,8 @@ describe('complex', () => { }) it('checks for equality', () => { - assert.ok(math.equal(math.complex(3,0), 3)) - assert.ok(math.equal(math.complex(3,2), math.complex(3, 2))) + assert.ok(math.equal(math.complex(3, 0), 3)) + assert.ok(math.equal(math.complex(3, 2), math.complex(3, 2))) assert.ok(!(math.equal(math.complex(45n, 3n), math.complex(45n, -3n)))) assert.ok(!(math.equal(math.complex(45n, 3n), 45n))) }) @@ -83,6 +83,7 @@ describe('complex', () => { assert.deepStrictEqual( math.multiply(q0, math.quaternion(2, 1, 0.1, 0.1)), math.quaternion(1.9, 1.1, 2.1, -0.9)) + math.absquare(math.complex(1.25, 2.5)) //HACK: need absquare(Complex) assert.strictEqual(math.abs(q0), Math.sqrt(2)) assert.strictEqual(math.abs(q1), Math.sqrt(33)/4) }) diff --git a/test/core/_utils.mjs b/test/core/_utils.mjs new file mode 100644 index 0000000..3976517 --- /dev/null +++ b/test/core/_utils.mjs @@ -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(''), []) + }) +}) diff --git a/test/custom.mjs b/test/custom.mjs index f6612ef..b6fd111 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -135,13 +135,8 @@ describe('A custom instance', () => { assert.strictEqual( inst.typeMerge(3, inst.complex(4.5,2.1)), 'Merge to Complex') - // The following is the current behavior, since 3 converts to 3+0i - // which is technically the same Complex type as 3n+0ni. - // This should clear up when Complex is templatized - assert.strictEqual(inst.typeMerge(3, inst.complex(3n)), 'Merge to Complex') - // But types that truly cannot be merged should throw a TypeError - // Should add a variation of this with a more usual type once there is - // one not interconvertible with others... + assert.throws( + () => inst.typeMerge(3, inst.complex(3n)), TypeError) inst.install(genericSubtract) assert.throws(() => inst.typeMerge(3, undefined), TypeError) }) diff --git a/test/generic/_all.mjs b/test/generic/_all.mjs new file mode 100644 index 0000000..94829a4 --- /dev/null +++ b/test/generic/_all.mjs @@ -0,0 +1,20 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' + +describe('generic', () => { + it('calculates mean', () => { + assert.strictEqual(math.mean(1,2.5,3.25,4.75), 2.875) + assert.strictEqual( + math.returnTypeOf('mean', 'number,number,number,number'), + 'number' + ) + }) + it('compares things', () => { + assert.strictEqual(math.larger(7n, 3n), true) + assert.strictEqual( + math.returnTypeOf('larger', 'bigint,bigint'), 'boolean') + assert.strictEqual(math.smallerEq(7.2, 3), false) + assert.strictEqual( + math.returnTypeOf('smallerEq', 'number,NumInt'), 'boolean') + }) +}) diff --git a/test/generic/fraction.mjs b/test/generic/fraction.mjs index 9af3b8d..f1fcd97 100644 --- a/test/generic/fraction.mjs +++ b/test/generic/fraction.mjs @@ -92,4 +92,10 @@ describe('fraction', () => { assert.deepStrictEqual(math.square(tf), math.fraction(9/16)) }) + it('knows the types of its operations', () => { + assert.deepStrictEqual( + math.returnTypeOf('ceiling', 'Fraction'), 'Fraction') + assert.deepStrictEqual( + math.returnTypeOf('multiply', 'Fraction,Fraction'), 'Fraction') + }) }) diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 2cf56d1..8a9b34c 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -8,14 +8,12 @@ describe('tuple', () => { it('does not allow unification by converting consecutive arguments', () => { assert.throws(() => math.tuple(3, 5.2, 2n), /TypeError.*unif/) - // Hence, the order matters in a slightly unfortunate way, - // but I think being a little ragged in these edge cases is OK: assert.throws( () => math.tuple(3, 2n, math.complex(5.2)), /TypeError.*unif/) - assert.deepStrictEqual( - math.tuple(3, math.complex(2n), 5.2), - {elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) + assert.throws( + () => math.tuple(3, math.complex(2n), 5.2), + /TypeError.*unif/) }) it('can be tested for zero and equality', () => { @@ -56,6 +54,9 @@ describe('tuple', () => { assert.deepStrictEqual( math.subtract(math.tuple(3n,4n,5n), math.tuple(2n,1n,0n)), math.tuple(1n,3n,5n)) + assert.deepStrictEqual( + math.returnTypeOf('subtract', 'Tuple,Tuple'), + 'Tuple') assert.throws( () => math.subtract(math.tuple(5,6), math.tuple(7)), /RangeError/) @@ -106,9 +107,16 @@ describe('tuple', () => { }) it('supports sqrt', () => { + const mixedTuple = math.tuple(2, math.complex(0,2), 1.5) assert.deepStrictEqual( - math.sqrt(math.tuple(4,-4,2.25)), - math.tuple(2, math.complex(0,2), 1.5)) + mixedTuple, + math.tuple(math.complex(2), math.complex(0,2), math.complex(1.5))) + assert.strictEqual( + math.returnTypeOf('tuple', 'NumInt, Complex, number'), + 'Tuple>') + assert.deepStrictEqual(math.sqrt(math.tuple(4,-4,2.25)), mixedTuple) + assert.strictEqual( + math.returnTypeOf('sqrt', 'Tuple'), 'Tuple>') }) })