From 31add66f4cc1f5768c8e0697214dbcd624754bf0 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 30 Aug 2022 19:36:44 +0000 Subject: [PATCH] feat: Return type annotations (#53) Provides the infrastructure to allow annotating the return types of functions, and does so for essentially every operation in the system (the only known exceptions being add, multiply, etc., on arbitrarily many arguments). One main infrastructure enhancements are bounded template types, e.g. `T:number` being a template parameter where T can take on the type `number` or any subtype thereof. A main internal enhancement is that base template types are no longer added to the typed universe; rather, there is a secondary, "meta" typed universe where they live. The primary point/purpose of this change is then the necessary search order for implementations can be much better modeled by typed-function's search order, using the `onMismatch` facility to redirect the search from fully instantiated implementations to the generic catchall implementations for each template (these catchalls live in the meta universe). Numerous other small improvements and bugfixes were encountered along the way. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/53 --- src/bigint/absquare.mjs | 3 +- src/bigint/add.mjs | 3 +- src/bigint/compare.mjs | 4 +- src/bigint/divide.mjs | 7 +- src/bigint/isZero.mjs | 3 +- src/bigint/multiply.mjs | 3 +- src/bigint/native.mjs | 4 +- src/bigint/negate.mjs | 3 +- src/bigint/one.mjs | 3 +- src/bigint/quotient.mjs | 7 +- src/bigint/roundquotient.mjs | 5 +- src/bigint/sign.mjs | 5 +- src/bigint/sqrt.mjs | 13 +- src/bigint/zero.mjs | 3 +- src/complex/Types/Complex.mjs | 20 +- src/complex/abs.mjs | 19 +- src/complex/absquare.mjs | 24 +- src/complex/add.mjs | 4 +- src/complex/associate.mjs | 5 +- src/complex/complex.mjs | 14 +- src/complex/conjugate.mjs | 4 +- src/complex/equalTT.mjs | 12 +- src/complex/gcd.mjs | 8 +- src/complex/invert.mjs | 6 +- src/complex/isZero.mjs | 4 +- src/complex/multiply.mjs | 17 +- src/complex/quaternion.mjs | 11 +- src/complex/quotient.mjs | 4 +- src/complex/roundquotient.mjs | 6 +- src/complex/sqrt.mjs | 43 +- src/core/PocomathInstance.mjs | 1229 +++++++++++++++++++++-------- src/core/Returns.mjs | 34 + src/core/utils.mjs | 2 + src/generic/Types/adapted.mjs | 43 +- src/generic/abs.mjs | 4 +- src/generic/absquare.mjs | 5 +- src/generic/all.mjs | 10 +- src/generic/divide.mjs | 5 +- src/generic/gcdType.mjs | 6 +- src/generic/identity.mjs | 12 +- src/generic/lcm.mjs | 4 +- src/generic/mean.mjs | 7 +- src/generic/mod.mjs | 5 +- src/generic/quotient.mjs | 8 +- src/generic/reducingOperation.mjs | 15 +- src/generic/relational.mjs | 37 +- src/generic/roundquotient.mjs | 8 +- src/generic/sign.mjs | 8 +- src/generic/sqrt.mjs | 3 +- src/generic/square.mjs | 5 +- src/generic/subtract.mjs | 8 +- src/number/abs.mjs | 3 +- src/number/absquare.mjs | 3 +- src/number/add.mjs | 7 +- src/number/compare.mjs | 5 +- src/number/invert.mjs | 4 +- src/number/isZero.mjs | 4 +- src/number/multiply.mjs | 4 +- src/number/native.mjs | 4 +- src/number/negate.mjs | 5 +- src/number/one.mjs | 4 +- src/number/quotient.mjs | 17 +- src/number/roundquotient.mjs | 6 +- src/number/sqrt.mjs | 17 +- src/number/zero.mjs | 3 +- src/ops/choose.mjs | 10 +- src/ops/factorial.mjs | 11 +- src/ops/floor.mjs | 23 +- src/tuple/Types/Tuple.mjs | 111 +-- src/tuple/equalTT.mjs | 9 +- src/tuple/isZero.mjs | 5 +- src/tuple/length.mjs | 3 +- src/tuple/tuple.mjs | 6 +- test/_pocomath.mjs | 49 +- test/complex/_all.mjs | 5 +- test/core/_utils.mjs | 8 + test/custom.mjs | 9 +- test/generic/_all.mjs | 20 + test/generic/fraction.mjs | 6 + test/tuple/_native.mjs | 22 +- 80 files changed, 1502 insertions(+), 606 deletions(-) create mode 100644 src/core/Returns.mjs create mode 100644 test/core/_utils.mjs create mode 100644 test/generic/_all.mjs 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>') }) })