From 4b28f8eac0c96be5eda85376e28cee3b0b268de7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 10 Aug 2022 12:00:06 -0700 Subject: [PATCH 01/15] refactor: Add a stub for supplying return type information With this commit, there's a rudimentary function that attaches return-type information, so it can be supplied, and PocomathInstance then proceeds to check for it everywhere necessary (and supply it in all the internal operations it builds). But so far in all of those place where it's checked for, it's just ignored. I think the next step will be just to put the return type info as a property on the function itself. That should actually ease all of the code. --- src/core/PocomathInstance.mjs | 54 +++++++++++++++++++++++++++++------ src/core/returns.mjs | 11 +++++++ src/number/negate.mjs | 36 ++++++++++++++++++++++- 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/core/returns.mjs diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 1add7eb..215ef74 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,7 +1,8 @@ /* 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 {R_, isReturnAnnotated} from './returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -82,7 +83,9 @@ export default class PocomathInstance { this._chainRepository = {} // place to store chainified functions this._installFunctions({ - typeOf: {ground: {uses: new Set(), does: () => () => 'any'}} + typeOf: { + ground: {uses: new Set(), does: () => R_('string', () => 'any')} + } }) this.joinTypes = this.joinTypes.bind(this) @@ -381,7 +384,7 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(), does: () => () => type} + imp[type] = {uses: new Set(), does: () => R_('string', () => type)} this._installFunctions({typeOf: imp}) } @@ -446,7 +449,10 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(['T']), does: ({T}) => () => `${base}<${T}>`} + imp[type] = { + uses: new Set(['T']), + does: ({T}) => R_('string', () => `${base}<${T}>`) + } this._installFunctions({typeOf: imp}) // Nothing else actually happens until we match a template parameter @@ -687,6 +693,9 @@ export default class PocomathInstance { } /* Now build the catchall implementation */ const self = this + /* For return-type annotation, we will have to fix this to + propagate the return type. At the moment we are just bagging + */ const patch = (refs) => (...args) => { /* We unbundle the rest arg if there is one */ const regLength = args.length - 1 @@ -806,7 +815,12 @@ export default class PocomathInstance { } } // Finally ready to make the call. - return behavior.does(innerRefs)(...args) + let returnSpec = behavior.does(innerRefs) + if (isReturnAnnotated(returnSpec)) { + // for now just drop return type information + returnSpec = returnSpec.via + } + return returnSpec(...args) } // The actual uses value needs to be a set: const outerUses = new Set(Object.values(simplifiedUses)) @@ -848,7 +862,12 @@ export default class PocomathInstance { _addTFimplementation(imps, signature, behavior) { const {uses, does} = behavior if (uses.length === 0) { - imps[signature] = does() + let returnSpec = does() + if (isReturnAnnotated(returnSpec)) { + // for now just dump the return information + returnSpec = returnSpec.via + } + imps[signature] = returnSpec return } const refs = {} @@ -908,7 +927,13 @@ export default class PocomathInstance { if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { refs.self = self - return does(refs) + let returnSpec = does(refs) + if (isReturnAnnotated(returnSpec)) { + // What are we going to do with the return type info in here? + // Anyhow, drop it for now + returnSpec = returnSpec.via + } + return returnSpec }) return } @@ -928,7 +953,12 @@ export default class PocomathInstance { } return } - imps[signature] = does(refs) + let returnSpec = does(refs) + if (isReturnAnnotated(returnSpec)) { + // drop return type for now + returnSpec = returnSpec.via + } + imps[signature] = returnSpec } _correctPartialSelfRefs(name, imps) { @@ -963,7 +993,13 @@ export default class PocomathInstance { for (let i = 0; i < part_self_references.length; ++i) { refs[`self(${part_self_references[i]})`] = impls[i] } - return does(refs) + let returnSpec = does(refs) + if (isReturnAnnotated(returnSpec)) { + // What will we do with the return type info in here? + // anyhow, drop it for now + returnSpec = returnSpec.via + } + return returnSpec } ) } diff --git a/src/core/returns.mjs b/src/core/returns.mjs new file mode 100644 index 0000000..d51e667 --- /dev/null +++ b/src/core/returns.mjs @@ -0,0 +1,11 @@ +/* Annotate a function with its return type */ +export function R_(type, fn) { + return {returns: type, via: fn} +} + +export function isReturnAnnotated(x) { + return x && typeof x === 'object' && 'returns' in x && 'via' in x +} + +export default R_ + diff --git a/src/number/negate.mjs b/src/number/negate.mjs index 82e27d0..5f937a4 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,3 +1,37 @@ +import R_ from '../core/returns.mjs' export * from './Types/number.mjs' -export const negate = {number: () => n => -n} +export const negate = { + NumInt: () => R_('NumInt', n => -n), + number: () => R_('number', n => -n) +} +/* Can we find a mechanism to avoid reiterating the definition + for each number (sub)type? Maybe something like: + + export const negate = {'T:number': ({T}) => R_(T, n => -n)} + + where 'T:number' is a new notation that means "T is a (non-strict) subtype + of number". That doesn't look too bad, but if we went down that route, we + might also need to make sure that if we ever implemented a PositiveNumber + type and a NegativeNumber type then we could say + + export const negate = { + PositiveNumber: () => R_('NegativeNumber', n => -n), + NegativeNumber: () => R_('PositiveNumber', n => -n), + 'T:number': ({T}) => R_(T, n => -n) + } + + + This just gets worse if there are also PosNumInt and NegNumInt types; + maybe Positive, Negative, and Zero are template types, so that + we could say: + + export const negate = { + 'Positive': ({'Negative': negT}) => R_(negT, n => -n), + 'Negative': ({'Positive': posT}) => R_(posT, n => -n), + T:number: ({T}) => R_(T, n => -n) + } + + But all of that is pretty speculative, for now let's just go with writing + out the different types. +*/ -- 2.34.1 From 95f6ccc5a0df825f90e2de6bee535daf63159f4b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 11 Aug 2022 08:13:10 -0700 Subject: [PATCH 02/15] refactor: Put return types as properties on implementations --- src/core/PocomathInstance.mjs | 51 ++++++++++++----------------------- src/core/returns.mjs | 26 +++++++++++++++--- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 215ef74..9f9520b 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {makeChain} from './Chain.mjs' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' -import {R_, isReturnAnnotated} from './returns.mjs' +import R_ from './returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -693,7 +693,7 @@ export default class PocomathInstance { } /* Now build the catchall implementation */ const self = this - /* For return-type annotation, we will have to fix this to + /* For return type annotation, we may have to fix this to propagate the return type. At the moment we are just bagging */ const patch = (refs) => (...args) => { @@ -815,12 +815,9 @@ export default class PocomathInstance { } } // Finally ready to make the call. - let returnSpec = behavior.does(innerRefs) - if (isReturnAnnotated(returnSpec)) { - // for now just drop return type information - returnSpec = returnSpec.via - } - return returnSpec(...args) + const implementation = behavior.does(innerRefs) + // Could do something with return type information here + return implementation(...args) } // The actual uses value needs to be a set: const outerUses = new Set(Object.values(simplifiedUses)) @@ -862,12 +859,9 @@ export default class PocomathInstance { _addTFimplementation(imps, signature, behavior) { const {uses, does} = behavior if (uses.length === 0) { - let returnSpec = does() - if (isReturnAnnotated(returnSpec)) { - // for now just dump the return information - returnSpec = returnSpec.via - } - imps[signature] = returnSpec + const implementation = does() + // could do something with return type information here + imps[signature] = implementation return } const refs = {} @@ -927,13 +921,9 @@ export default class PocomathInstance { if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { refs.self = self - let returnSpec = does(refs) - if (isReturnAnnotated(returnSpec)) { - // What are we going to do with the return type info in here? - // Anyhow, drop it for now - returnSpec = returnSpec.via - } - return returnSpec + const implementation = does(refs) + // What are we going to do with the return type info in here? + return implementation }) return } @@ -953,12 +943,9 @@ export default class PocomathInstance { } return } - let returnSpec = does(refs) - if (isReturnAnnotated(returnSpec)) { - // drop return type for now - returnSpec = returnSpec.via - } - imps[signature] = returnSpec + const implementation = does(refs) + // could do something with return type information here + imps[signature] = implementation } _correctPartialSelfRefs(name, imps) { @@ -993,13 +980,9 @@ export default class PocomathInstance { for (let i = 0; i < part_self_references.length; ++i) { refs[`self(${part_self_references[i]})`] = impls[i] } - let returnSpec = does(refs) - if (isReturnAnnotated(returnSpec)) { - // What will we do with the return type info in here? - // anyhow, drop it for now - returnSpec = returnSpec.via - } - return returnSpec + const implementation = does(refs) + // What will we do with the return type info in here? + return implementation } ) } diff --git a/src/core/returns.mjs b/src/core/returns.mjs index d51e667..1c236c4 100644 --- a/src/core/returns.mjs +++ b/src/core/returns.mjs @@ -1,10 +1,28 @@ /* Annotate a function with its return type */ -export function R_(type, fn) { - return {returns: type, via: fn} + +/* 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 isReturnAnnotated(x) { - return x && typeof x === 'object' && 'returns' in x && 'via' in x +export function R_(type, fn) { + if ('returns' in fn) fn = cloneFunction(fn) + fn.returns = type + return fn +} + +export function returnTypeOf(fn) { + return fn.returns || 'any' } export default R_ -- 2.34.1 From 340dbd436ee16c635d27595a1c663c118b1aef3e Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 11 Aug 2022 23:51:47 -0700 Subject: [PATCH 03/15] feat(PocomathInstance): Add .returnTypeOf method --- src/core/PocomathInstance.mjs | 12 +++++++++++- test/_pocomath.mjs | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 9f9520b..f8d9c5a 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {makeChain} from './Chain.mjs' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' -import R_ from './returns.mjs' +import {R_, returnTypeOf} from './returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -191,6 +191,16 @@ export default class PocomathInstance { return result } + /* Determine the return type of an operation given an input signature */ + returnTypeOf(operation, signature) { + if (typeof operation === 'string') { + operation = this[operation] + } + if (!(this._typed.isTypedFunction(operation))) return 'any' + const details = this._typed.findSignature(operation, signature) + return returnTypeOf(details.fn) + } + /* Return a chain object for this instance with a given value: */ chain(value) { return makeChain(value, this, this._chainRepository) diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 38df18b..9e52a25 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -20,6 +20,10 @@ 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') + }) + it('can subtract numbers', () => { assert.strictEqual(math.subtract(12, 5), 7) //assert.strictEqual(math.subtract(3n, 1.5), 1.5) -- 2.34.1 From dc6921e768561cfd95ad0db05f52f2c667be3624 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 12 Aug 2022 09:10:53 -0700 Subject: [PATCH 04/15] feat(PocomathInstance): Add subtypesOf and isSubtypeOf methods Note this involves refeactoring the internal subtype tracker to keep subtypes in a list sorted in topological order. --- src/core/PocomathInstance.mjs | 49 +++++++++++++++++++++++++++-------- test/_pocomath.mjs | 1 + 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index f8d9c5a..398bcee 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -30,9 +30,12 @@ export default class PocomathInstance { 'install', 'installType', 'instantiateTemplate', + 'isSubtypeOf', 'joinTypes', 'name', + 'returnTypeOf', 'self', + 'subtypesOf', 'Templates', 'typeOf', 'Types', @@ -56,7 +59,8 @@ export default class PocomathInstance { this.Templates = {} // 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 @@ -334,14 +338,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 */ @@ -369,8 +373,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.`) } @@ -398,8 +402,30 @@ export default class PocomathInstance { this._installFunctions({typeOf: imp}) } - /* 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 is typeA is a subtype of type B */ + isSubtypeOf(typeA, typeB) { + return this._subtypes[typeB].includes(typeA) + } + + /* Returns a list of the subtypes of a given type, in topological sorted + * order (i.e, no type on the list contains one that comes after it). + */ + subtypesOf(type) { + // HERE! For this to work, have to maintain subtypes as a sorted list. + return this._subtypes[type] // should we clone? + } + /* 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). */ @@ -418,19 +444,20 @@ export default class PocomathInstance { 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 } } @@ -1132,7 +1159,7 @@ export default class PocomathInstance { break } if (myType === otherType - || this._subtypes[otherType].has(myType)) { + || this.isSubtypeOf(myType, otherType)) { continue } if (otherType in this.Templates) { diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 9e52a25..c5e8969 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -22,6 +22,7 @@ describe('The default full pocomath instance "math"', () => { it('can determine the return types of operations', () => { assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number') + assert.strictEqual(math.returnTypeOf('negate', 'NumInt'), 'NumInt') }) it('can subtract numbers', () => { -- 2.34.1 From f7bb3697ed554c13dc6e364341add2874adaf4cc Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 22 Aug 2022 12:38:23 -0400 Subject: [PATCH 05/15] feat(PocomathInstance): Add bounded template parameters This feature helps specify the return type of implementations where the return type depends on the exact subtype that the implementation was called with, such as negate. --- src/core/PocomathInstance.mjs | 92 +++++++++++++++++++-------- src/core/{returns.mjs => Returns.mjs} | 13 ++-- src/number/negate.mjs | 35 +--------- test/_pocomath.mjs | 2 +- 4 files changed, 77 insertions(+), 65 deletions(-) rename src/core/{returns.mjs => Returns.mjs} (62%) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 398bcee..5755857 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {makeChain} from './Chain.mjs' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' -import {R_, returnTypeOf} from './returns.mjs' +import {Returns, returnTypeOf} from './Returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -13,7 +13,9 @@ const templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type */ -function substituteInSig(sig, parameter, type) { +const upperBounds = /\s*[(]=\s*(\w*)\s*/g +function substituteInSignature(signature, parameter, type) { + const sig = signature.replaceAll(upperBounds, '') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } @@ -30,12 +32,14 @@ export default class PocomathInstance { 'install', 'installType', 'instantiateTemplate', + 'isPriorTo', 'isSubtypeOf', 'joinTypes', 'name', 'returnTypeOf', 'self', 'subtypesOf', + 'supertypesOf', 'Templates', 'typeOf', 'Types', @@ -88,7 +92,7 @@ export default class PocomathInstance { this._installFunctions({ typeOf: { - ground: {uses: new Set(), does: () => R_('string', () => 'any')} + ground: {uses: new Set(), does: () => Returns('string', () => 'any')} } }) @@ -202,7 +206,7 @@ export default class PocomathInstance { } if (!(this._typed.isTypedFunction(operation))) return 'any' const details = this._typed.findSignature(operation, signature) - return returnTypeOf(details.fn) + return returnTypeOf(details.fn, signature, this) } /* Return a chain object for this instance with a given value: */ @@ -398,7 +402,7 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(), does: () => R_('string', () => type)} + imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) } @@ -412,18 +416,36 @@ export default class PocomathInstance { supSubs.splice(i, 0, sub) } - /* Returns true is typeA is a subtype of type B */ + /* Returns true if typeA is a subtype of type B */ isSubtypeOf(typeA, typeB) { return this._subtypes[typeB].includes(typeA) } + /* Returns true if typeA is a subtype of or converts to type B */ + isPriorTo(typeA, typeB) { + if (!(typeB in this._priorTypes)) return false + return this._priorTypes[typeB].has(typeA) + } + /* Returns a list of the subtypes of a given type, in topological sorted * order (i.e, no type on the list contains one that comes after it). */ subtypesOf(type) { - // HERE! For this to work, have to maintain subtypes as a sorted list. return this._subtypes[type] // should we clone? } + + /* Returns a list of the supertypes of a given type, starting with itself, + * in topological order + */ + supertypesOf(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 @@ -488,7 +510,7 @@ export default class PocomathInstance { const imp = {} imp[type] = { uses: new Set(['T']), - does: ({T}) => R_('string', () => `${base}<${T}>`) + does: ({T}) => Returns('string', () => `${base}<${T}>`) } this._installFunctions({typeOf: imp}) @@ -628,7 +650,7 @@ export default class PocomathInstance { /* Check if it's an ordinary non-template signature */ let explicit = true for (const type of typesOfSignature(rawSignature)) { - for (const word of type.split(/[<>]/)) { + for (const word of type.split(/[<>(=\s]/)) { if (this._templateParam(word)) { explicit = false break @@ -640,14 +662,26 @@ export default class PocomathInstance { continue } /* It's a template, have to instantiate */ + /* 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[1] + 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 (!('instantiations' in behavior)) { behavior.instantiations = new Set() + if (ubType) behavior.instantiations.add(ubType) } let instantiationSet = new Set() for (const instType of behavior.instantiations) { instantiationSet.add(instType) - for (const other of this._priorTypes[instType]) { + for (const other of this.subtypesOf(instType)) { instantiationSet.add(other) } } @@ -656,7 +690,7 @@ export default class PocomathInstance { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue const signature = - substituteInSig(rawSignature, theTemplateParam, instType) + substituteInSignature(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ if (signature in imps) continue /* Don't go too deep */ @@ -670,7 +704,7 @@ export default class PocomathInstance { const uses = new Set() for (const dep of behavior.uses) { if (this._templateParam(dep)) continue - uses.add(substituteInSig(dep, theTemplateParam, instType)) + uses.add(substituteInSignature(dep, theTemplateParam, instType)) } const patch = (refs) => { const innerRefs = {} @@ -678,7 +712,7 @@ export default class PocomathInstance { if (this._templateParam(dep)) { innerRefs[dep] = instType } else { - const outerName = substituteInSig( + const outerName = substituteInSignature( dep, theTemplateParam, instType) innerRefs[dep] = refs[outerName] } @@ -689,11 +723,13 @@ export default class PocomathInstance { tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ + /* (Not needed if if it's a bounded template) */ + if (ubType) continue 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( + const signature = substituteInSignature( baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call * and add the new instantiations. @@ -718,7 +754,8 @@ export default class PocomathInstance { for (const dep of behavior.uses) { let [func, needsig] = dep.split(/[()]/) if (needsig) { - const subsig = substituteInSig(needsig, theTemplateParam, '') + const subsig = substituteInSignature( + needsig, theTemplateParam, '') if (subsig === needsig) { simplifiedUses[dep] = dep } else { @@ -789,7 +826,7 @@ export default class PocomathInstance { self._maxDepthSeen = depth } /* Generate the list of actual wanted types */ - const wantTypes = parTypes.map(type => substituteInSig( + const wantTypes = parTypes.map(type => substituteInSignature( type, theTemplateParam, instantiateFor)) /* Now we have to add any actual types that are relevant * to this invocation. Namely, that would be every formal parameter @@ -811,7 +848,7 @@ export default class PocomathInstance { if (wantType.slice(0,3) === '...') { wantType = wantType.slice(3) } - wantType = substituteInSig( + wantType = substituteInSignature( wantType, theTemplateParam, instantiateFor) if (wantType !== parTypes[i]) { args[j] = self._typed.convert(args[j], wantType) @@ -840,7 +877,7 @@ export default class PocomathInstance { } else { let [func, needsig] = dep.split(/[()]/) if (self._typed.isTypedFunction(refs[simplifiedDep])) { - const subsig = substituteInSig( + const subsig = substituteInSignature( needsig, theTemplateParam, instantiateFor) let resname = simplifiedDep if (resname == 'self') resname = name @@ -890,8 +927,8 @@ export default class PocomathInstance { } /* 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 @@ -910,7 +947,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: ' + @@ -1051,8 +1088,9 @@ 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). */ @@ -1081,7 +1119,7 @@ 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) { @@ -1090,7 +1128,7 @@ export default class PocomathInstance { newTypeSpec.test = template.test(this._typeTests[instantiator]) 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(/[<>]/)) { @@ -1101,14 +1139,14 @@ export default class PocomathInstance { } 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( + const finalSource = substituteInSignature( instSource, templateFromParam, subType) maybeFrom[finalSource] = template.from[source](x => x) } diff --git a/src/core/returns.mjs b/src/core/Returns.mjs similarity index 62% rename from src/core/returns.mjs rename to src/core/Returns.mjs index 1c236c4..211babd 100644 --- a/src/core/returns.mjs +++ b/src/core/Returns.mjs @@ -15,15 +15,20 @@ function cloneFunction(fn) { return theClone } -export function R_(type, fn) { +export function Returns(type, fn) { if ('returns' in fn) fn = cloneFunction(fn) fn.returns = type return fn } -export function returnTypeOf(fn) { - return fn.returns || 'any' +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 R_ +export default Returns diff --git a/src/number/negate.mjs b/src/number/negate.mjs index 5f937a4..696e953 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,37 +1,6 @@ -import R_ from '../core/returns.mjs' +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const negate = { - NumInt: () => R_('NumInt', n => -n), - number: () => R_('number', n => -n) + 'T (= number': ({T}) => Returns(T, n => -n) } -/* Can we find a mechanism to avoid reiterating the definition - for each number (sub)type? Maybe something like: - - export const negate = {'T:number': ({T}) => R_(T, n => -n)} - - where 'T:number' is a new notation that means "T is a (non-strict) subtype - of number". That doesn't look too bad, but if we went down that route, we - might also need to make sure that if we ever implemented a PositiveNumber - type and a NegativeNumber type then we could say - - export const negate = { - PositiveNumber: () => R_('NegativeNumber', n => -n), - NegativeNumber: () => R_('PositiveNumber', n => -n), - 'T:number': ({T}) => R_(T, n => -n) - } - - - This just gets worse if there are also PosNumInt and NegNumInt types; - maybe Positive, Negative, and Zero are template types, so that - we could say: - - export const negate = { - 'Positive': ({'Negative': negT}) => R_(negT, n => -n), - 'Negative': ({'Positive': posT}) => R_(posT, n => -n), - T:number: ({T}) => R_(T, n => -n) - } - - But all of that is pretty speculative, for now let's just go with writing - out the different types. -*/ diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index c5e8969..63e3b30 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -27,7 +27,7 @@ describe('The default full pocomath instance "math"', () => { 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', () => { -- 2.34.1 From 775bb9ddb7dba1c479a861b5217a3ee525a5cbe6 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 24 Aug 2022 13:37:32 -0400 Subject: [PATCH 06/15] feat(returnTypeOf): Support template and upper bound types Also changed the notation for an upper bound template to the more readable 'T:number' (instead of 'T (= number', which was supposed to look like the non-strict subset relation). --- src/bigint/negate.mjs | 3 +- src/complex/Types/Complex.mjs | 8 +- src/complex/add.mjs | 4 +- src/complex/complex.mjs | 14 ++-- src/core/PocomathInstance.mjs | 139 ++++++++++++++++++++++++++-------- src/core/utils.mjs | 2 + src/number/add.mjs | 7 +- src/number/negate.mjs | 2 +- src/number/zero.mjs | 3 +- src/ops/floor.mjs | 22 +++--- test/_pocomath.mjs | 11 +++ test/core/_utils.mjs | 8 ++ 12 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 test/core/_utils.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/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 0fa4107..c76341c 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -1,3 +1,4 @@ +import {Returns, returnTypeOf} from '../../core/Returns.mjs' import PocomathInstance from '../../core/PocomathInstance.mjs' const Complex = new PocomathInstance('Complex') @@ -21,7 +22,12 @@ Complex.installType('Complex', { }) 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/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/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/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 5755857..936ccfb 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -8,14 +8,15 @@ import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type const theTemplateParam = 'T' // First pass: only allow this one exact parameter +const restTemplateParam = `...${theTemplateParam}` const templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type */ -const upperBounds = /\s*[(]=\s*(\w*)\s*/g +const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g function substituteInSignature(signature, parameter, type) { - const sig = signature.replaceAll(upperBounds, '') + const sig = signature.replaceAll(upperBounds, '$1') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } @@ -53,6 +54,14 @@ export default class PocomathInstance { this._typed = typed.create() this._typed.clear() this._typed.addTypes([{name: 'ground', test: () => true}]) + const me = this + const myTyped = this._typed + this._typed.onMismatch = (name, args, sigs) => { + if (me._invalid.has(name)) { + return me[name](...args) // rebuild implementation and try again + } + myTyped.throwMismatchError(name, args, sigs) + } /* List of types installed in the instance. We start with just dummies * for the 'any' type and for type parameters: */ @@ -201,11 +210,13 @@ export default class PocomathInstance { /* Determine the return type of an operation given an input signature */ returnTypeOf(operation, signature) { - if (typeof operation === 'string') { - operation = this[operation] + for (const type of typeListOfSignature(signature)) { + this._maybeInstantiate(type) } - if (!(this._typed.isTypedFunction(operation))) return 'any' - const details = this._typed.findSignature(operation, signature) + if (typeof operation !== 'string') { + operation = operation.name + } + const details = this._pocoFindSignature(operation, signature) return returnTypeOf(details.fn, signature, this) } @@ -650,7 +661,7 @@ export default class PocomathInstance { /* Check if it's an ordinary non-template signature */ let explicit = true for (const type of typesOfSignature(rawSignature)) { - for (const word of type.split(/[<>(=\s]/)) { + for (const word of type.split(/[<>:\s]/)) { if (this._templateParam(word)) { explicit = false break @@ -667,7 +678,7 @@ export default class PocomathInstance { upperBounds.lastIndex = 0 let ubType = upperBounds.exec(rawSignature) if (ubType) { - ubType = ubType[1] + ubType = ubType[2] if (!ubType in this.Types) { throw new TypeError( `Unknown type upper bound '${ubType}' in '${rawSignature}'`) @@ -681,7 +692,9 @@ export default class PocomathInstance { let instantiationSet = new Set() for (const instType of behavior.instantiations) { instantiationSet.add(instType) - for (const other of this.subtypesOf(instType)) { + const otherTypes = + ubType ? this.subtypesOf(instType) : this._priorTypes[instType] + for (const other of otherTypes) { instantiationSet.add(other) } } @@ -828,6 +841,7 @@ export default class PocomathInstance { /* Generate the list of actual wanted types */ const wantTypes = parTypes.map(type => substituteInSignature( type, theTemplateParam, instantiateFor)) + const wantSig = wantTypes.join(',') /* Now we have to add any actual types that are relevant * to this invocation. Namely, that would be every formal parameter * type in the invocation, with the parameter template instantiated @@ -890,9 +904,22 @@ export default class PocomathInstance { } // Finally ready to make the call. const implementation = behavior.does(innerRefs) - // Could do something with return type information here + // We can access return type information here + // And in particular, if it's a template, we should try to + // instantiate it: + const returnType = returnTypeOf(implementation, wantSig, self) + const instantiated = self._maybeInstantiate(returnType) + if (instantiated) { + const tempBase = instantiated.split('<',1)[0] + self._invalidateDependents(':' + tempBase) + } return implementation(...args) } + Object.defineProperty(patch, 'name', {value: `${name}(${signature})`}) + // TODO: Decorate patch with a function that calculates the + // correct return type a priori. Deferring because unclear what + // aspects will be merged into typed-function. + // // The actual uses value needs to be a set: const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( @@ -905,16 +932,8 @@ 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) } } } @@ -926,6 +945,29 @@ export default class PocomathInstance { return tf } + /* Takes an arbitrary type and performs an instantiation if necessary. + * @param {string} type The type to instantiate + * @param {string | bool | undefined } + * Returns the name of the type if an instantiation occurred, false + * if the type was already present, and undefined if the type can't + * be satisfied (because it is not the name of a type or it is nested + * too deep. + */ + _maybeInstantiate(type) { + if (type.slice(0,3) === '...') { + type = type.slice(3) + } + if (!(type.includes('<'))) { + // Not a template, so just check if type exists + if (type in this.Types) return false // already there + return undefined // no such type + } + // it's a template type, turn it into a template and an arg + let base = type.split('<',1)[0] + const arg = type.slice(base.length+1, -1) + return this.instantiateTemplate(base, arg) + } + /* Adapts Pocomath-style behavior specification (uses, does) for signature * to typed-function implementations and inserts the result into plain * object imps @@ -985,7 +1027,7 @@ export default class PocomathInstance { } else { // can bundle up func, and grab its signature if need be let destination = this[func] - if (destination &&needsig) { + if (destination && needsig) { destination = this._pocoresolve(func, needsig) } refs[dep] = destination @@ -996,6 +1038,7 @@ export default class PocomathInstance { imps[signature] = this._typed.referToSelf(self => { refs.self = self const implementation = does(refs) + Object.defineProperty(implementation, 'name', {value: does.name}) // What are we going to do with the return type info in here? return implementation }) @@ -1171,16 +1214,17 @@ export default class PocomathInstance { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true + let paramBound = 'ground' for (let k = 0; k < typeList.length; ++k) { let myType = typeList[k] let otherType = otherTypeList[k] if (otherType === theTemplateParam) { - otherTypeList[k] = 'ground' - otherType = 'ground' + otherTypeList[k] = paramBound + otherType = paramBound } - if (otherType === '...T') { - otherTypeList[k] = '...ground' - otherType = 'ground' + if (otherType === restTemplateParam) { + otherTypeList[k] = `...${paramBound}` + otherType = paramBound } const adjustedOtherType = otherType.replaceAll( `<${theTemplateParam}>`, '') @@ -1190,6 +1234,13 @@ export default class PocomathInstance { } if (myType.slice(0,3) === '...') myType = myType.slice(3) if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) + const otherBound = upperBounds.exec(otherType) + if (otherBound) { + paramBound = otherBound[2] + otherType = paramBound + otherTypeList[k] = otherBound[1].replaceAll( + theTemplateParam, paramBound) + } if (otherType === 'any') continue if (otherType === 'ground') continue if (!(otherType in this.Types)) { @@ -1218,25 +1269,49 @@ export default class PocomathInstance { return foundSig } - _pocoresolve(name, sig, typedFunction) { + _pocoFindSignature(name, sig, typedFunction) { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] } let result = undefined + if (!this._typed.isTypedFunction(typedFunction)) { + return result + } try { - result = this._typed.find(typedFunction, sig, {exact: true}) + result = this._typed.findSignature(typedFunction, sig, {exact: true}) } catch { } if (result) return result const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) - if (foundsig) return this._typed.find(typedFunction, foundsig) - // Make sure bundle is up-to-date: + if (foundsig) return this._typed.findSignature(typedFunction, foundsig) + const wantTypes = typeListOfSignature(sig) + for (const [implSig, details] + of typedFunction._typedFunctionData.signatureMap) { + let allMatched = true + const implTypes = typeListOfSignature(implSig) + for (let i = 0; i < wantTypes.length; ++i) { + if (wantTypes[i] == implTypes[i] + || this.isSubtypeOf(wantTypes[i], implTypes[i])) continue + allMatched = false + break + } + if (allMatched) return details + } + // Hmm, no luck. Make sure bundle is up-to-date and retry: typedFunction = this[name] try { - result = this._typed.find(typedFunction, sig) + result = this._typed.findSignature(typedFunction, sig) } catch { } - if (result) return result + return result + } + + _pocoresolve(name, sig, typedFunction) { + if (!this._typed.isTypedFunction(typedFunction)) { + typedFunction = this[name] + } + const result = this._pocoFindSignature(name, sig, typedFunction) + if (result) return result.implementation // total punt, revert to typed-function resolution on every call; // hopefully this happens rarely: return typedFunction 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/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/negate.mjs b/src/number/negate.mjs index 696e953..f2336c7 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -2,5 +2,5 @@ import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const negate = { - 'T (= number': ({T}) => Returns(T, n => -n) + 'T:number': ({T}) => Returns(T, n => -n) } 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/floor.mjs b/src/ops/floor.mjs index 5fdb9e7..c2a23fc 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,17 +6,20 @@ import {Complex} from '../complex/Types/Complex.mjs' */ export const floor = { - bigint: () => x => x, - NumInt: () => x => x, // Because Pocomath isn't part of typed-function, or - 'Complex': () => 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'], @@ -25,6 +29,6 @@ export const floor = { BigNumber: ({ 'round(BigNumber)': rnd, 'equal(BigNumber,BigNumber)': eq - }) => x => eq(x,round(x)) ? round(x) : x.floor() + }) => Returns('BigNumber', x => eq(x,round(x)) ? round(x) : x.floor()) } diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 63e3b30..4d471dc 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -23,6 +23,17 @@ describe('The default full pocomath instance "math"', () => { it('can determine the return types of operations', () => { assert.strictEqual(math.returnTypeOf('negate', 'number'), 'number') assert.strictEqual(math.returnTypeOf('negate', 'NumInt'), 'NumInt') + math.negate(math.complex(1.2, 2.8)) // TODO: make this call unnecessary + assert.strictEqual( + math.returnTypeOf('negate', 'Complex'), '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') }) it('can subtract numbers', () => { 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(''), []) + }) +}) -- 2.34.1 From 0950d7d585b4daedb6dc271be9fe823777ee63ee Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 24 Aug 2022 19:34:33 -0400 Subject: [PATCH 07/15] feat(PocomathInstance): Specify return types of all core methods --- src/core/PocomathInstance.mjs | 68 +++++++++++++++++++---------------- src/ops/floor.mjs | 4 +-- test/_pocomath.mjs | 4 +++ 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 936ccfb..041177c 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -164,7 +164,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) } @@ -197,7 +197,7 @@ export default class PocomathInstance { } } this._installFunctions(stdFunctions) - } + }) /* Merge any number of PocomathInstances or modules: */ static merge(name, ...pieces) { @@ -209,7 +209,7 @@ export default class PocomathInstance { } /* Determine the return type of an operation given an input signature */ - returnTypeOf(operation, signature) { + returnTypeOf = Returns('string', function(operation, signature) { for (const type of typeListOfSignature(signature)) { this._maybeInstantiate(type) } @@ -217,13 +217,20 @@ export default class PocomathInstance { operation = operation.name } const details = this._pocoFindSignature(operation, signature) - return returnTypeOf(details.fn, signature, this) - } + if (details) { + return returnTypeOf(details.fn, signature, this) + } + console.log('Checking return type of', operation) + 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)) { @@ -272,9 +279,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 @@ -315,7 +322,7 @@ export default class PocomathInstance { * Implementation note: unlike _installFunctions below, we can make * the corresponding changes to the _typed object immediately */ - installType(type, spec) { + installType = Returns('void', function(type, spec) { const parts = type.split(/[<,>]/) if (this._templateParam(parts[0])) { throw new SyntaxError( @@ -415,7 +422,7 @@ export default class PocomathInstance { const imp = {} imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) - } + }) _addSubtypeTo(sup, sub) { if (this.isSubtypeOf(sub, sup)) return @@ -428,47 +435,48 @@ export default class PocomathInstance { } /* Returns true if typeA is a subtype of type B */ - isSubtypeOf(typeA, typeB) { + isSubtypeOf = Returns('boolean', function(typeA, typeB) { return this._subtypes[typeB].includes(typeA) - } + }) /* Returns true if typeA is a subtype of or converts to type B */ - isPriorTo(typeA, typeB) { + isPriorTo = Returns('boolean', function(typeA, typeB) { if (!(typeB in this._priorTypes)) return false return this._priorTypes[typeB].has(typeA) - } + }) - /* Returns a list of the subtypes of a given type, in topological sorted - * order (i.e, no type on the list contains one that comes after it). + /* 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(type) { + 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(type) { + 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 @@ -500,9 +508,9 @@ 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() { + undefinedTypes = Returns('Array', function() { return Array.from(this._seenTypes).filter(t => !(t in this.Types)) - } + }) /* Used internally to install a template type */ _installTemplateType(type, spec) { @@ -1137,7 +1145,7 @@ export default class PocomathInstance { * 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 @@ -1204,7 +1212,7 @@ export default class PocomathInstance { } this.installType(wantsType, newTypeSpec) return wantsType - } + }) _findSubtypeImpl(name, imps, neededSig) { if (neededSig in imps) return neededSig @@ -1305,7 +1313,7 @@ export default class PocomathInstance { } return result } - + _pocoresolve(name, sig, typedFunction) { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index c2a23fc..2fbe106 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -13,8 +13,8 @@ export const floor = { * be separately activated */ bigint: () => Returns('bigint', x => x), - NumInt: () => Returns('NumInt', x => x), - 'Complex': () => Returns('Complex', x => x), + NumInt: () => Returns('NumInt', x => x), + 'Complex': () => Returns('Complex', x => x), number: ({'equalTT(number,number)': eq}) => Returns('NumInt', n => { if (eq(n, Math.round(n))) return Math.round(n) diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 4d471dc..eb0481b 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -34,6 +34,10 @@ describe('The default full pocomath instance "math"', () => { math.add(3, math.complex(2.5, 1)), math.complex(5.5, 1)) assert.strictEqual( math.returnTypeOf('add', 'Complex,NumInt'), 'Complex') + assert.strictEqual( + math.returnTypeOf('chain', 'bigint'), 'Chain') + assert.strictEqual( + math.returnTypeOf('returnTypeOf', 'string,string'), 'string') }) it('can subtract numbers', () => { -- 2.34.1 From bc434c716335615f006d3a1235338c62950412fe Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 24 Aug 2022 20:59:30 -0400 Subject: [PATCH 08/15] feat(bigint): Specify return types for all methods --- src/bigint/absquare.mjs | 3 ++- src/bigint/add.mjs | 3 ++- src/bigint/compare.mjs | 4 +++- src/bigint/divide.mjs | 5 +++-- src/bigint/isZero.mjs | 3 ++- src/bigint/multiply.mjs | 3 ++- src/bigint/native.mjs | 4 ++-- 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/generic/gcdType.mjs | 6 ++++-- src/generic/identity.mjs | 12 ++++++++++-- src/number/native.mjs | 4 ++-- test/_pocomath.mjs | 8 ++++++++ 17 files changed, 61 insertions(+), 30 deletions(-) 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..028d3e6 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) => { + 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/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/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/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/test/_pocomath.mjs b/test/_pocomath.mjs index eb0481b..b49efb6 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -38,6 +38,14 @@ describe('The default full pocomath instance "math"', () => { 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') }) it('can subtract numbers', () => { -- 2.34.1 From a2f76a55b84242460d4639c8110e416f8e488d21 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 26 Aug 2022 23:54:32 -0400 Subject: [PATCH 09/15] feat(return types): Add more return types for complex functions These changes greatly increased the need for precision in generating implementations for signatures of operations whenever possible. So this commit also includes a refactor that basically caches all of the conversions of Pocomath implementations to typed-function implementatios so that they are more often externally available (i.e., not disrupted so much after invalidation). --- src/bigint/divide.mjs | 2 +- src/complex/Types/Complex.mjs | 2 +- src/complex/abs.mjs | 23 +++- src/complex/absquare.mjs | 28 ++++- src/complex/multiply.mjs | 9 +- src/complex/sqrt.mjs | 43 ++++--- src/core/PocomathInstance.mjs | 229 +++++++++++++++++++++++++--------- src/generic/absquare.mjs | 5 +- src/generic/square.mjs | 5 +- src/number/absquare.mjs | 3 +- src/number/multiply.mjs | 4 +- src/number/sqrt.mjs | 17 +-- test/complex/_all.mjs | 1 + 13 files changed, 273 insertions(+), 98 deletions(-) diff --git a/src/bigint/divide.mjs b/src/bigint/divide.mjs index 028d3e6..492893a 100644 --- a/src/bigint/divide.mjs +++ b/src/bigint/divide.mjs @@ -3,7 +3,7 @@ export * from './Types/bigint.mjs' export const divide = { 'bigint,bigint': ({config, 'quotient(bigint,bigint)': quot}) => { - if (config.predictable) return quot + 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 diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index c76341c..6dbaa60 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -6,7 +6,7 @@ const Complex = new PocomathInstance('Complex') 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 +// Now the template type: Complex numbers are actually always homogeneous // in their component types. Complex.installType('Complex', { infer: ({typeOf, joinTypes}) => z => joinTypes([typeOf(z.re), typeOf(z.im)]), diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 536b8b4..d793d33 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -1,10 +1,25 @@ +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 + sqrt, // Unfortunately no notation yet for the needed signature + 'absquare(T)': baseabsq, 'absquare(Complex)': absq - }) => z => sqrt(absq(z)) + }) => { + const pm = sqrt.fromInstance + if (typeof pm === 'undefined') { + // Just checking for the dependencies, return value is irrelevant + return undefined + } + const midType = returnTypeOf(baseabsq) + const sqrtImp = pm.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..b013c4e 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -1,9 +1,31 @@ +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 pm = add.fromInstance + if (typeof pm === 'undefined') { + // Just checking the dependencies, return value irrelevant + return undefined + } + const midType = returnTypeOf(absq) + const addImp = pm.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/multiply.mjs b/src/complex/multiply.mjs index e059a91..db59a44 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -1,15 +1,18 @@ +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, 'self(T,T)': me, 'conjugate(T)': conj // makes quaternion multiplication work - }) => (w,z) => { - return cplx( + }) => Returns( + `Complex<${T}>`, + (w,z) => 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))) - } + ) } 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 041177c..25e3d18 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -38,6 +38,7 @@ export default class PocomathInstance { 'joinTypes', 'name', 'returnTypeOf', + 'resolve', 'self', 'subtypesOf', 'supertypesOf', @@ -49,7 +50,8 @@ 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() @@ -220,7 +222,6 @@ export default class PocomathInstance { if (details) { return returnTypeOf(details.fn, signature, this) } - console.log('Checking return type of', operation) return returnTypeOf(this[operation], signature, this) }) @@ -564,8 +565,28 @@ 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, + uses: behavior.uses, + does: behavior.does + } + if (explicit) { + opImps[signature].resolved = false + } else { + 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)) { @@ -604,11 +625,40 @@ 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] = {} } + 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 @@ -628,7 +678,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) } } } @@ -642,6 +692,10 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } + if (!(this._TFimps[name])) { + this._TFimps[name] = {} + } + const tf_imps = this._TFimps[name] /* Collect the entries we know the types for */ const usableEntries = [] for (const entry of Object.entries(imps)) { @@ -664,20 +718,13 @@ 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(/[<>:\s]/)) { - 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 */ @@ -693,12 +740,9 @@ export default class PocomathInstance { } } /* First, add the known instantiations, gathering all types needed */ - if (!('instantiations' in behavior)) { - behavior.instantiations = new Set() - if (ubType) behavior.instantiations.add(ubType) - } + if (ubType) behavior.needsInstantiations.add(ubType) let instantiationSet = new Set() - for (const instType of behavior.instantiations) { + for (const instType of behavior.needsInstantiations) { instantiationSet.add(instType) const otherTypes = ubType ? this.subtypesOf(instType) : this._priorTypes[instType] @@ -710,6 +754,7 @@ export default class PocomathInstance { for (const instType of instantiationSet) { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue + if (instType in behavior.hasInstantiations) continue const signature = substituteInSignature(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ @@ -742,10 +787,14 @@ export default class PocomathInstance { } this._addTFimplementation( tf_imps, signature, {uses, does: patch}) + tf_imps[signature]._pocoSignature = rawSignature + tf_imps[signature]._pocoInstance = instType + behavior.hasInstantiations[instType] = signature } /* Now add the catchall signature */ /* (Not needed if if it's a bounded template) */ if (ubType) continue + if ('_catchall_' in behavior.hasInstantiations) continue let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ let baseSignature = rawSignature.replaceAll(templateCall, '') @@ -886,11 +935,11 @@ export default class PocomathInstance { /* 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) + behavior.needsInstantiations.add(instantiateFor) self._invalidate(name) // And update refs because we now know the type we're instantiating // for: + refs[theTemplateParam] = instantiateFor const innerRefs = {} for (const dep in simplifiedUses) { const simplifiedDep = simplifiedUses[dep] @@ -903,7 +952,7 @@ export default class PocomathInstance { needsig, theTemplateParam, instantiateFor) let resname = simplifiedDep if (resname == 'self') resname = name - innerRefs[dep] = self._pocoresolve( + innerRefs[dep] = self.resolve( resname, subsig, refs[simplifiedDep]) } else { innerRefs[dep] = refs[simplifiedDep] @@ -913,13 +962,15 @@ export default class PocomathInstance { // Finally ready to make the call. const implementation = behavior.does(innerRefs) // We can access return type information here - // And in particular, if it's a template, we should try to + // And in particular, if it might be a template, we should try to // instantiate it: const returnType = returnTypeOf(implementation, wantSig, self) - const instantiated = self._maybeInstantiate(returnType) - if (instantiated) { - const tempBase = instantiated.split('<',1)[0] - self._invalidateDependents(':' + tempBase) + for (const possibility of returnType.split('|')) { + const instantiated = self._maybeInstantiate(possibility) + if (instantiated) { + const tempBase = instantiated.split('<',1)[0] + self._invalidateDependents(':' + tempBase) + } } return implementation(...args) } @@ -932,6 +983,7 @@ export default class PocomathInstance { const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( tf_imps, signature, {uses: outerUses, does: patch}) + behavior.hasInstantiations._catchall_ = rawSignature } this._correctPartialSelfRefs(name, tf_imps) // Make sure we have all of the needed (template) types; and if they @@ -946,9 +998,17 @@ export default class PocomathInstance { } } 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) + Object.defineProperty(tf, 'fromInstance', {value: this}) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } @@ -1006,13 +1066,20 @@ 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') + } + if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { + part_self_references.push(needsig) + } } } else { if (part_self_references.length) { @@ -1024,19 +1091,41 @@ 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) { + const tempTF = this._typed('dummy_' + func, this._TFimps[func]) + let result = undefined + try { + result = this._typed.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) + destination = this.resolve(func, needsig) } refs[dep] = destination } @@ -1076,7 +1165,8 @@ export default class PocomathInstance { _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 = [] for (const neededSig of part_self_references) { // Have to find a match for neededSig among the other signatures @@ -1098,8 +1188,8 @@ export default class PocomathInstance { + `${name}(${neededSig})`) } } - const refs = imps[aSignature].builtRefs - const does = imps[aSignature].sigDoes + const refs = deferral.builtRefs + const does = deferral.sigDoes imps[aSignature] = this._typed.referTo( ...corrected_self_references, (...impls) => { for (let i = 0; i < part_self_references.length; ++i) { @@ -1110,6 +1200,8 @@ export default class PocomathInstance { return implementation } ) + imps[aSignature]._pocoSignature = deferral._pocoSignature + imps[aSignature]._pocoInstance = deferral._pocoInstance } } @@ -1282,16 +1374,30 @@ export default class PocomathInstance { typedFunction = this[name] } let result = undefined - if (!this._typed.isTypedFunction(typedFunction)) { - return result + const haveTF = this._typed.isTypedFunction(typedFunction) + if (haveTF) { + try { + result = this._typed.findSignature(typedFunction, sig, {exact: true}) + } catch { + } } - try { - result = this._typed.findSignature(typedFunction, sig, {exact: true}) - } catch { - } - if (result) return result + if (result || !(this._imps[name])) return result const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) - if (foundsig) return this._typed.findSignature(typedFunction, foundsig) + if (foundsig) { + if (haveTF) { + return this._typed.findSignature(typedFunction, foundsig) + } + // 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} + } const wantTypes = typeListOfSignature(sig) for (const [implSig, details] of typedFunction._typedFunctionData.signatureMap) { @@ -1314,7 +1420,12 @@ export default class PocomathInstance { return result } - _pocoresolve(name, sig, typedFunction) { + /* 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] } @@ -1323,6 +1434,6 @@ export default class PocomathInstance { // total punt, revert to typed-function resolution on every call; // hopefully this happens rarely: return typedFunction - } + }) } 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/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/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/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/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/test/complex/_all.mjs b/test/complex/_all.mjs index 531a28d..884350a 100644 --- a/test/complex/_all.mjs +++ b/test/complex/_all.mjs @@ -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) }) -- 2.34.1 From de42c22ab4af1c86eafda38acd80e10b22fa3cf7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 09:30:07 -0400 Subject: [PATCH 10/15] refactor: Convert resolution to two-tier system Typed-function's sort order/matching algorithm was interfering with template resolution. This commit solves the difficulty by moving the "catchall" implementations that implement generation of new template instances into a separate "fallback" typed-function universe, so that Pocomath can control exactly when that is searched. Removes a couple of the matching anomalies already noted in the tests. Also extends return types to somewhat more functions. --- src/complex/Types/Complex.mjs | 10 +- src/complex/abs.mjs | 1 + src/complex/associate.mjs | 5 +- src/complex/multiply.mjs | 10 +- src/core/PocomathInstance.mjs | 616 +++++++++++++++++++++------------- src/generic/abs.mjs | 4 +- src/number/abs.mjs | 3 +- src/tuple/Types/Tuple.mjs | 30 +- src/tuple/length.mjs | 3 +- test/_pocomath.mjs | 3 + test/complex/_all.mjs | 4 +- test/custom.mjs | 9 +- test/tuple/_native.mjs | 8 +- 13 files changed, 428 insertions(+), 278 deletions(-) diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index 6dbaa60..4d63417 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -2,15 +2,13 @@ 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 homogeneous -// in their component types. +// 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 => { diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index d793d33..30034ed 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -3,6 +3,7 @@ export * from './Types/Complex.mjs' export const abs = { 'Complex': ({ + T, sqrt, // Unfortunately no notation yet for the needed signature 'absquare(T)': baseabsq, 'absquare(Complex)': absq 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/multiply.mjs b/src/complex/multiply.mjs index db59a44..5d9edc6 100644 --- a/src/complex/multiply.mjs +++ b/src/complex/multiply.mjs @@ -6,13 +6,15 @@ export const multiply = { 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 }) => Returns( `Complex<${T}>`, - (w,z) => 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))) + (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/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 25e3d18..f591941 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -9,6 +9,7 @@ const anySpec = {} // fixed dummy specification of 'any' type 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 a new signature just like sig but with the parameter replaced by @@ -21,6 +22,8 @@ function substituteInSignature(signature, parameter, type) { 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 @@ -55,24 +58,39 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() - this._typed.addTypes([{name: 'ground', test: () => true}]) + // 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)) { - return me[name](...args) // rebuild implementation and try again + // rebuild implementation and try again + return me[name](...args) } - myTyped.throwMismatchError(name, args, sigs) + 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: - */ + // 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 = {} // For each type, gives all of its (in)direct subtypes in topo order: this._subtypes = {} @@ -87,27 +105,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: () => Returns('string', () => 'any')} - } - }) - - this.joinTypes = this.joinTypes.bind(this) + this.joinTypes = this.joinTypes.bind(me) } /** @@ -330,9 +340,10 @@ export default class PocomathInstance { `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}`) @@ -345,7 +356,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 @@ -387,6 +398,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 toParts = nextSuper.split('<', 2) + if (toParts.length > 1) { + const fromParts = from.split('<', 2) + if (fromParts.length === 1 || fromParts[0] !== toParts[0]) { + this._metafy(from) + try { + this._metaTyped.addConversion( + {from, to: toParts[0], convert: spec.from[from]}) + } catch { + } + } + } + nextSuper = this.Types[nextSuper].refines } } @@ -411,6 +437,16 @@ export default class PocomathInstance { convert: this.Types[to].from[fromtype] }) this._invalidateDependents(':' + nextSuper) + /* Add the conversion in the metaverse if need be: */ + const toParts = nextSuper.split('<', 2) + if (toParts.length > 1 && base !== toParts[0]) { + this._metafy(type) + this._metaTyped.addConversion({ + from: type, + to: toParts[0], + convert: this.Types[to].from[fromtype] + }) + } } catch { } this._priorTypes[nextSuper].add(type) @@ -425,6 +461,12 @@ export default class PocomathInstance { 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) + } + _addSubtypeTo(sup, sub) { if (this.isSubtypeOf(sub, sup)) return const supSubs = this._subtypes[sup] @@ -435,8 +477,10 @@ export default class PocomathInstance { supSubs.splice(i, 0, sub) } - /* Returns true if typeA is a subtype of type B */ + /* 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) }) @@ -483,7 +527,6 @@ export default class PocomathInstance { 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 const pick = convert ? 'has' : 'includes' @@ -510,7 +553,8 @@ export default class PocomathInstance { * signatures of operations, but which have not actually been installed: */ undefinedTypes = Returns('Array', function() { - return Array.from(this._seenTypes).filter(t => !(t in this.Types)) + return Array.from(this._seenTypes).filter( + t => !(t in this.Types || t in this.Templates)) }) /* Used internally to install a template type */ @@ -526,6 +570,18 @@ 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] = { @@ -534,6 +590,9 @@ export default class PocomathInstance { } 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} } @@ -578,12 +637,11 @@ export default class PocomathInstance { } opImps[signature] = { explicit, + resolved: false, uses: behavior.uses, does: behavior.does } - if (explicit) { - opImps[signature].resolved = false - } else { + if (!explicit) { opImps[signature].hasInstantiations = {} opImps[signature].needsInstantiations = new Set() } @@ -628,6 +686,8 @@ export default class PocomathInstance { _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: @@ -692,10 +752,8 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } - if (!(this._TFimps[name])) { - this._TFimps[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)) { @@ -751,6 +809,79 @@ export default class PocomathInstance { } } + /* 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) { + // collect upper-bound types + const ubTypes = new Set() + 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 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 = othertype.split('<',1)[0] + otherTypeCollection = this._instantiationsOf[base] + } + for (const possibility of otherTypeCollection) { + for (const convtype of this._priorTypes[possibility]) { + if (this.isSubtypeOf(convtype, possibility)) continue + if (ubTypes.has(convtype)) continue + let belowUB = false + for (const anUB of ubTypes) { + if (anUB in this.Templates) { + if (convtype.slice(0, anUB.length) === anUB) { + belowUB = true + break + } + } else { + if (this.isSubtypeOf(convtype, anUB)) { + belowUB = true + break + } + } + } + if (belowUB) continue + instantiationSet.add(convtype) + } + } + } + } + } + for (const instType of instantiationSet) { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue @@ -785,8 +916,7 @@ export default class PocomathInstance { } return behavior.does(innerRefs) } - this._addTFimplementation( - tf_imps, signature, {uses, does: patch}) + this._addTFimplementation(tf_imps, signature, {uses, does: patch}) tf_imps[signature]._pocoSignature = rawSignature tf_imps[signature]._pocoInstance = instType behavior.hasInstantiations[instType] = signature @@ -794,13 +924,7 @@ export default class PocomathInstance { /* Now add the catchall signature */ /* (Not needed if if it's a bounded template) */ if (ubType) continue - if ('_catchall_' in behavior.hasInstantiations) continue - 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 = substituteInSignature( - baseSignature, theTemplateParam, 'ground') + 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: @@ -840,150 +964,124 @@ export default class PocomathInstance { /* For return type annotation, we may have to fix this to propagate the return type. At the moment we are just bagging */ - 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) + 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 => 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) - } - } - /* 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 = substituteInSignature( - 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 - */ - behavior.needsInstantiations.add(instantiateFor) - self._invalidate(name) - // And update refs because we now know the type we're instantiating - // for: - refs[theTemplateParam] = instantiateFor - 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 = substituteInSignature( - needsig, theTemplateParam, instantiateFor) - let resname = simplifiedDep - if (resname == 'self') resname = name - innerRefs[dep] = self.resolve( - 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) } } - } - // Finally ready to make the call. - const implementation = behavior.does(innerRefs) - // 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(implementation, wantSig, self) - for (const possibility of returnType.split('|')) { - const instantiated = self._maybeInstantiate(possibility) - if (instantiated) { - const tempBase = instantiated.split('<',1)[0] - self._invalidateDependents(':' + tempBase) + + /* 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 = instantiated.split('<',1)[0] + 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 } - return implementation(...args) + Object.defineProperty( + patchFunc, 'name', {value: `${name}(${signature})`}) + return patchFunc } - Object.defineProperty(patch, 'name', {value: `${name}(${signature})`}) - // TODO: Decorate patch with a function that calculates the + 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. - // - // The actual uses value needs to be a set: - const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( - tf_imps, signature, {uses: outerUses, does: patch}) - behavior.hasInstantiations._catchall_ = rawSignature + 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 @@ -1007,8 +1105,19 @@ export default class PocomathInstance { delete fromBehavior.hasInstantiations[imp._pocoInstance] } } - const tf = this._typed(name, tf_imps) - Object.defineProperty(tf, 'fromInstance', {value: this}) + let tf + if (Object.keys(tf_imps).length > 0) { + tf = this._typed(name, tf_imps) + Object.defineProperty(tf, 'fromInstance', {value: this}) + } + let metaTF + if (Object.keys(meta_imps).length > 0) { + metaTF = this._metaTyped(name, meta_imps) + Object.defineProperty(metaTF, 'fromInstance', {value: this}) + } + this._meta[name] = metaTF + + tf = tf || metaTF Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } @@ -1077,7 +1186,10 @@ export default class PocomathInstance { 'typed-function does not support mixed full and ' + 'partial self-reference') } - if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { + const needTypes = typesOfSignature(needsig) + const mergedTypes = Object.assign( + {}, this.Types, this.Templates) + if (subsetOfKeys(needTypes, mergedTypes)) { part_self_references.push(needsig) } } @@ -1097,10 +1209,19 @@ export default class PocomathInstance { * accumulating: */ if (needsig) { - const tempTF = this._typed('dummy_' + func, this._TFimps[func]) + 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 = this._typed.find(tempTF, needsig, {exact: true}) + result = typedUniverse.find(tempTF, needsig, {exact: true}) } catch {} if (result) { refs[dep] = result @@ -1158,7 +1279,7 @@ export default class PocomathInstance { return } const implementation = does(refs) - // could do something with return type information here + // could do something with return type information here? imps[signature] = implementation } @@ -1168,38 +1289,53 @@ export default class PocomathInstance { 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) + refs[`self(${neededSig})`] = match.implementation + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + `${name}(${neededSig})`) + } } } - const refs = deferral.builtRefs const does = deferral.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] + 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 } - const implementation = does(refs) - // What will we do with the return type info in here? - return implementation - } - ) + ) + } else { + imps[aSignature] = does(refs) + } imps[aSignature]._pocoSignature = deferral._pocoSignature imps[aSignature]._pocoInstance = deferral._pocoInstance } @@ -1247,7 +1383,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) { @@ -1255,6 +1391,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}>`) @@ -1268,18 +1409,15 @@ export default class PocomathInstance { 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 = 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 = substituteInSignature( @@ -1287,11 +1425,13 @@ export default class PocomathInstance { 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 = substituteInSignature( - 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] @@ -1303,6 +1443,7 @@ export default class PocomathInstance { newTypeSpec.from = maybeFrom } this.installType(wantsType, newTypeSpec) + this._instantiationsOf[base].add(wantsType) return wantsType }) @@ -1314,7 +1455,7 @@ export default class PocomathInstance { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true - let paramBound = 'ground' + let paramBound = 'any' for (let k = 0; k < typeList.length; ++k) { let myType = typeList[k] let otherType = otherTypeList[k] @@ -1326,8 +1467,7 @@ export default class PocomathInstance { otherTypeList[k] = `...${paramBound}` otherType = paramBound } - const adjustedOtherType = otherType.replaceAll( - `<${theTemplateParam}>`, '') + const adjustedOtherType = otherType.replaceAll(templateCall, '') if (adjustedOtherType !== otherType) { otherTypeList[k] = adjustedOtherType otherType = adjustedOtherType @@ -1342,22 +1482,21 @@ export default class PocomathInstance { theTemplateParam, paramBound) } if (otherType === 'any') continue - if (otherType === 'ground') continue - if (!(otherType in this.Types)) { - allMatch = false - break - } - if (myType === otherType - || this.isSubtypeOf(myType, otherType)) { - continue - } + if (myType === otherType) continue if (otherType in this.Templates) { + const myBase = myType.split('<',1)[0] + 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 } @@ -1373,19 +1512,42 @@ export default class PocomathInstance { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] } - let result = undefined 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) { + if (wantTypes[i] == implTypes[i] + || this.isSubtypeOf(wantTypes[i], implTypes[i])) continue + allMatched = false + break + } + if (allMatched) return details + } } - if (result || !(this._imps[name])) return result + if (!(this._imps[name])) return undefined const foundsig = this._findSubtypeImpl(name, this._imps[name], sig) if (foundsig) { if (haveTF) { - return this._typed.findSignature(typedFunction, foundsig) + 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: @@ -1398,20 +1560,8 @@ export default class PocomathInstance { const pseudoImpl = foundImpl.does(needs) return {fn: pseudoImpl, implementation: pseudoImpl} } - const wantTypes = typeListOfSignature(sig) - for (const [implSig, details] - of typedFunction._typedFunctionData.signatureMap) { - let allMatched = true - const implTypes = typeListOfSignature(implSig) - for (let i = 0; i < wantTypes.length; ++i) { - if (wantTypes[i] == implTypes[i] - || this.isSubtypeOf(wantTypes[i], implTypes[i])) continue - allMatched = false - break - } - if (allMatched) return details - } // Hmm, no luck. Make sure bundle is up-to-date and retry: + let result = undefined typedFunction = this[name] try { result = this._typed.findSignature(typedFunction, sig) 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/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/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index d76d147..49255f7 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -2,24 +2,22 @@ import PocomathInstance from '../../core/PocomathInstance.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: 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/test/_pocomath.mjs b/test/_pocomath.mjs index b49efb6..549e6ba 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -46,6 +46,9 @@ describe('The default full pocomath instance "math"', () => { 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') }) it('can subtract numbers', () => { diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs index 884350a..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))) }) 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/tuple/_native.mjs b/test/tuple/_native.mjs index 2cf56d1..a37b213 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', () => { -- 2.34.1 From 23b3ef4fddffd928267963ee50d23f8b28defe3a Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 11:10:34 -0400 Subject: [PATCH 11/15] feat(Complex): Define return types of all operations --- src/complex/conjugate.mjs | 4 +++- src/complex/equalTT.mjs | 11 ++++++----- src/complex/gcd.mjs | 8 +++++--- src/complex/invert.mjs | 6 ++++-- src/complex/isZero.mjs | 4 +++- src/complex/quaternion.mjs | 11 ++++++++++- src/complex/quotient.mjs | 4 +++- src/complex/roundquotient.mjs | 6 ++++-- test/_pocomath.mjs | 6 ++++++ 9 files changed, 44 insertions(+), 16 deletions(-) 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..b979857 100644 --- a/src/complex/equalTT.mjs +++ b/src/complex/equalTT.mjs @@ -1,9 +1,10 @@ +import Returns from '../core/Returns.mjs' export * from './Types/Complex.mjs' export const equalTT = { 'Complex,Complex': ({ '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 +12,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/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/test/_pocomath.mjs b/test/_pocomath.mjs index 549e6ba..4a63d55 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -49,6 +49,12 @@ describe('The default full pocomath instance "math"', () => { 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) }) it('can subtract numbers', () => { -- 2.34.1 From 3957ae8adffb18c22c23a7b9274171d429660742 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 19:42:48 -0400 Subject: [PATCH 12/15] feat: Add return types for all generic operations --- src/generic/Types/adapted.mjs | 43 +++++++++++++++++-------------- src/generic/all.mjs | 10 ++++--- src/generic/divide.mjs | 5 +++- 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/subtract.mjs | 8 +++++- test/_pocomath.mjs | 5 ++++ test/generic/_all.mjs | 20 ++++++++++++++ test/generic/fraction.mjs | 6 +++++ 16 files changed, 141 insertions(+), 51 deletions(-) create mode 100644 test/generic/_all.mjs 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/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/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/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/test/_pocomath.mjs b/test/_pocomath.mjs index 4a63d55..3eff3d3 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -34,6 +34,11 @@ describe('The default full pocomath instance "math"', () => { 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( 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') + }) }) -- 2.34.1 From 1ee6da429498c02910d623871916be8912f5a606 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 21:18:59 -0400 Subject: [PATCH 13/15] feat(number): Provide return types for all operations --- src/complex/equalTT.mjs | 1 + src/core/PocomathInstance.mjs | 101 +++++++++++++++++----------------- src/number/compare.mjs | 5 +- src/number/invert.mjs | 4 +- src/number/isZero.mjs | 4 +- src/number/one.mjs | 4 +- src/number/quotient.mjs | 17 ++---- src/number/roundquotient.mjs | 6 +- test/_pocomath.mjs | 3 + 9 files changed, 75 insertions(+), 70 deletions(-) diff --git a/src/complex/equalTT.mjs b/src/complex/equalTT.mjs index b979857..6a84c9a 100644 --- a/src/complex/equalTT.mjs +++ b/src/complex/equalTT.mjs @@ -3,6 +3,7 @@ export * from './Types/Complex.mjs' export const equalTT = { 'Complex,Complex': ({ + T, 'self(T,T)': me }) => 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 diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index f591941..3ac6f94 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -800,26 +800,9 @@ export default class PocomathInstance { /* First, add the known instantiations, gathering all types needed */ if (ubType) behavior.needsInstantiations.add(ubType) let instantiationSet = new Set() - for (const instType of behavior.needsInstantiations) { - instantiationSet.add(instType) - const otherTypes = - ubType ? this.subtypesOf(instType) : this._priorTypes[instType] - for (const other of otherTypes) { - 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) { - // collect upper-bound types - const ubTypes = new Set() + 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]) @@ -839,6 +822,27 @@ export default class PocomathInstance { } } } + } + for (const instType of behavior.needsInstantiations) { + instantiationSet.add(instType) + 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( @@ -859,23 +863,9 @@ export default class PocomathInstance { for (const possibility of otherTypeCollection) { for (const convtype of this._priorTypes[possibility]) { if (this.isSubtypeOf(convtype, possibility)) continue - if (ubTypes.has(convtype)) continue - let belowUB = false - for (const anUB of ubTypes) { - if (anUB in this.Templates) { - if (convtype.slice(0, anUB.length) === anUB) { - belowUB = true - break - } - } else { - if (this.isSubtypeOf(convtype, anUB)) { - belowUB = true - break - } - } + if (!(this._atOrBelowSomeType(convtype, ubTypes))) { + instantiationSet.add(convtype) } - if (belowUB) continue - instantiationSet.add(convtype) } } } @@ -943,22 +933,7 @@ 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 = substituteInSignature( - needsig, theTemplateParam, '') - if (subsig === needsig) { - simplifiedUses[dep] = dep - } else { - simplifiedUses[dep] = func - } - } else { - simplifiedUses[dep] = dep - } - } + /* Now build the catchall implementation */ const self = this /* For return type annotation, we may have to fix this to @@ -1122,6 +1097,28 @@ export default class PocomathInstance { 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 } 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/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/test/_pocomath.mjs b/test/_pocomath.mjs index 3eff3d3..5d80e85 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -60,6 +60,9 @@ describe('The default full pocomath instance "math"', () => { 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') }) it('can subtract numbers', () => { -- 2.34.1 From be9794fd4cf500aa065455a7fe83d1b12f19a540 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 29 Aug 2022 21:30:32 -0400 Subject: [PATCH 14/15] feat(ops): Provide return types in all of the operation-centric examples --- src/ops/choose.mjs | 10 ++++++---- src/ops/factorial.mjs | 11 +++++++++-- src/ops/floor.mjs | 1 - test/_pocomath.mjs | 2 ++ 4 files changed, 17 insertions(+), 7 deletions(-) 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 2fbe106..3754dcb 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -25,7 +25,6 @@ export const floor = { // 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 diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 5d80e85..c4a5c28 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -63,6 +63,8 @@ describe('The default full pocomath instance "math"', () => { 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', () => { -- 2.34.1 From d83f2a7f2391f8edafeaf49c275894e37625d268 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 30 Aug 2022 15:15:23 -0400 Subject: [PATCH 15/15] feat: Instantiate instances of template instantiation need for self-reference Formerly, when a self-reference like `self(Tuple)` was encountered, but the `Tuple` instance of a `Tuple` implementation for this operation had not yet been instantiated, the reference would be fulfilled with a call to the catchall implementation for `Tuple`. Now the necessary instance is instantiated on the spot and referred to instead. This change is used to complete return-type specification for all of the Tuple functions. --- src/complex/abs.mjs | 7 +- src/complex/absquare.mjs | 8 +- src/core/PocomathInstance.mjs | 238 +++++++++++++++++++++++++--------- src/tuple/Types/Tuple.mjs | 81 +++++++----- src/tuple/equalTT.mjs | 9 +- src/tuple/isZero.mjs | 5 +- src/tuple/tuple.mjs | 6 +- test/tuple/_native.mjs | 14 +- 8 files changed, 255 insertions(+), 113 deletions(-) diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs index 30034ed..53a2464 100644 --- a/src/complex/abs.mjs +++ b/src/complex/abs.mjs @@ -8,13 +8,8 @@ export const abs = { 'absquare(T)': baseabsq, 'absquare(Complex)': absq }) => { - const pm = sqrt.fromInstance - if (typeof pm === 'undefined') { - // Just checking for the dependencies, return value is irrelevant - return undefined - } const midType = returnTypeOf(baseabsq) - const sqrtImp = pm.resolve('sqrt', midType, sqrt) + 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 diff --git a/src/complex/absquare.mjs b/src/complex/absquare.mjs index b013c4e..ab0194c 100644 --- a/src/complex/absquare.mjs +++ b/src/complex/absquare.mjs @@ -9,13 +9,9 @@ export const absquare = { // we extract the needed implementation below. 'self(T)': absq }) => { - const pm = add.fromInstance - if (typeof pm === 'undefined') { - // Just checking the dependencies, return value irrelevant - return undefined - } const midType = returnTypeOf(absq) - const addImp = pm.resolve('add', `${midType},${midType}`, add) + const addImp = add.fromInstance.resolve( + 'add', `${midType},${midType}`, add) return Returns( returnTypeOf(addImp), z => addImp(absq(z.re), absq(z.im))) } diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 3ac6f94..4b46752 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -7,11 +7,76 @@ 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 */ @@ -202,7 +267,10 @@ 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 @@ -334,7 +402,7 @@ export default class PocomathInstance { * the corresponding changes to the _typed object immediately */ installType = Returns('void', function(type, spec) { - const parts = type.split(/[<,>]/) + const parts = type.split(/[<,>]/).map(s => s.trim()) if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) @@ -400,14 +468,14 @@ export default class PocomathInstance { } /* Add the conversion in the metaverse if need be: */ - const toParts = nextSuper.split('<', 2) - if (toParts.length > 1) { - const fromParts = from.split('<', 2) - if (fromParts.length === 1 || fromParts[0] !== toParts[0]) { + 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: toParts[0], convert: spec.from[from]}) + {from, to: toBase, convert: spec.from[from]}) } catch { } } @@ -438,12 +506,12 @@ export default class PocomathInstance { }) this._invalidateDependents(':' + nextSuper) /* Add the conversion in the metaverse if need be: */ - const toParts = nextSuper.split('<', 2) - if (toParts.length > 1 && base !== toParts[0]) { + const [toBase, toInstance] = splitTemplate(nextSuper) + if (toInstance && base !== toBase) { this._metafy(type) this._metaTyped.addConversion({ from: type, - to: toParts[0], + to: toBase, convert: this.Types[to].from[fromtype] }) } @@ -559,7 +627,7 @@ export default class PocomathInstance { /* 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: */ @@ -760,7 +828,7 @@ export default class PocomathInstance { 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 @@ -857,7 +925,7 @@ export default class PocomathInstance { othertype, theTemplateParam, '') let otherTypeCollection = [othertype] if (testType !== othertype) { - const base = othertype.split('<',1)[0] + const [base] = splitTemplate(othertype) otherTypeCollection = this._instantiationsOf[base] } for (const possibility of otherTypeCollection) { @@ -873,43 +941,7 @@ export default class PocomathInstance { } for (const instType of instantiationSet) { - if (!(instType in this.Types)) continue - if (this.Types[instType] === anySpec) continue - if (instType in behavior.hasInstantiations) continue - const signature = - substituteInSignature(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(substituteInSignature(dep, theTemplateParam, instType)) - } - const patch = (refs) => { - const innerRefs = {} - for (const dep of behavior.uses) { - if (this._templateParam(dep)) { - innerRefs[dep] = instType - } else { - const outerName = substituteInSignature( - dep, theTemplateParam, instType) - innerRefs[dep] = refs[outerName] - } - } - return behavior.does(innerRefs) - } - this._addTFimplementation(tf_imps, signature, {uses, does: patch}) - tf_imps[signature]._pocoSignature = rawSignature - tf_imps[signature]._pocoInstance = instType - behavior.hasInstantiations[instType] = signature + this._instantiateTemplateImplementation(name, rawSignature, instType) } /* Now add the catchall signature */ /* (Not needed if if it's a bounded template) */ @@ -1031,7 +1063,7 @@ export default class PocomathInstance { for (const possibility of returnType.split('|')) { const instantiated = self._maybeInstantiate(possibility) if (instantiated) { - const tempBase = instantiated.split('<',1)[0] + const [tempBase] = splitTemplate(instantiated) self._invalidateDependents(':' + tempBase) } } @@ -1047,6 +1079,7 @@ export default class PocomathInstance { } Object.defineProperty( patchFunc, 'name', {value: `${name}(${signature})`}) + patchFunc._pocoSignature = rawSignature return patchFunc } Object.defineProperty( @@ -1083,12 +1116,12 @@ export default class PocomathInstance { let tf if (Object.keys(tf_imps).length > 0) { tf = this._typed(name, tf_imps) - Object.defineProperty(tf, 'fromInstance', {value: this}) + tf.fromInstance = this } let metaTF if (Object.keys(meta_imps).length > 0) { metaTF = this._metaTyped(name, meta_imps) - Object.defineProperty(metaTF, 'fromInstance', {value: this}) + metaTF.fromInstance = this } this._meta[name] = metaTF @@ -1137,11 +1170,58 @@ export default class PocomathInstance { return undefined // no such type } // it's a template type, turn it into a template and an arg - let base = type.split('<',1)[0] - const arg = type.slice(base.length+1, -1) + 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 @@ -1254,6 +1334,7 @@ export default class PocomathInstance { refs.self = self 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 }) @@ -1271,11 +1352,13 @@ export default class PocomathInstance { deferred: true, builtRefs: refs, sigDoes: does, + fromInstance: this, psr: part_self_references } return } const implementation = does(refs) + implementation.fromInstance = this // could do something with return type information here? imps[signature] = implementation } @@ -1310,7 +1393,22 @@ export default class PocomathInstance { name, this._imps[name], neededSig) if (foundSig) { const match = this._pocoFindSignature(name, neededSig) - refs[`self(${neededSig})`] = match.implementation + 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 ' @@ -1335,6 +1433,7 @@ export default class PocomathInstance { } imps[aSignature]._pocoSignature = deferral._pocoSignature imps[aSignature]._pocoInstance = deferral._pocoInstance + imps[aSignature].fromInstance = deferral.fromInstance } } @@ -1343,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) @@ -1481,7 +1579,7 @@ export default class PocomathInstance { if (otherType === 'any') continue if (myType === otherType) continue if (otherType in this.Templates) { - const myBase = myType.split('<',1)[0] + const [myBase] = splitTemplate(myType) if (myBase === otherType) continue if (this.instantiateTemplate(otherType, myType)) { let dummy @@ -1525,10 +1623,24 @@ export default class PocomathInstance { let allMatched = true const implTypes = typeListOfSignature(implSig) for (let i = 0; i < wantTypes.length; ++i) { - if (wantTypes[i] == implTypes[i] - || this.isSubtypeOf(wantTypes[i], implTypes[i])) continue - allMatched = false - break + 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 } diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index 49255f7..4ff6685 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -1,5 +1,6 @@ /* 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') @@ -33,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/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/tuple/_native.mjs b/test/tuple/_native.mjs index a37b213..8a9b34c 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -54,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/) @@ -104,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>') }) }) -- 2.34.1