From 1b8314c0ccf75a9e7208f1e5809f586b70677437 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 2 Aug 2022 09:52:39 -0700 Subject: [PATCH 01/10] refactor: Tighten definition of complex gcd to just GaussianInteger --- src/complex/gcd.mjs | 15 +++++++++------ src/complex/native.mjs | 2 -- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/complex/gcd.mjs b/src/complex/gcd.mjs index 84aa849..238d811 100644 --- a/src/complex/gcd.mjs +++ b/src/complex/gcd.mjs @@ -3,15 +3,18 @@ import * as Complex from './Types/Complex.mjs' import gcdType from '../generic/gcdType.mjs' const imps = { - gcdComplexRaw: gcdType('Complex'), + gcdGIRaw: gcdType('GaussianInteger'), gcd: { // Only return gcds with positive real part - 'Complex, Complex': ({gcdComplexRaw, sign, one, negate}) => (z,m) => { - const raw = gcdComplexRaw(z, m) - if (sign(raw.re) === one(raw.re)) return raw - return negate(raw) + 'GaussianInteger,GaussianInteger': ({ + 'gcdGIRaw(GaussianInteger,GaussianInteger)': gcdRaw, + 'sign(bigint)': sgn, + 'negate(GaussianInteger)': neg + }) => (z,m) => { + const raw = gcdRaw(z, m) + if (sgn(raw.re) === 1n) return raw + return neg(raw) } } } export const gcd = PocomathInstance.merge(Complex, imps) - diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 70d6b14..bea1db8 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -1,5 +1,3 @@ -import gcdType from '../generic/gcdType.mjs' - export * from './Types/Complex.mjs' export {abs} from './abs.mjs' -- 2.34.1 From 880efac15bba02e7c3f61a0fe4eead3e0040b5ba Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 2 Aug 2022 10:32:06 -0700 Subject: [PATCH 02/10] feat(gcd,lcm): Allow arbitrarily many arguments And add an 'associate' predicate for two complex numbers to check the results --- src/complex/associate.mjs | 17 +++++++++++++++++ src/complex/native.mjs | 1 + src/generic/arithmetic.mjs | 1 + src/generic/lcm.mjs | 3 +++ test/_pocomath.mjs | 9 +++++++++ 5 files changed, 31 insertions(+) create mode 100644 src/complex/associate.mjs diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs new file mode 100644 index 0000000..c6fab9e --- /dev/null +++ b/src/complex/associate.mjs @@ -0,0 +1,17 @@ +export * from './Types/Complex.mjs' + +/* Returns true if w is z multiplied by a complex unit */ +export const associate = { + 'Complex,Complex': ({ + 'multiply(Complex,Complex)': times, + 'equal(Complex,Complex)': eq, + zero, + one, + complex, + 'negate(Complex)': neg + }) => (w,z) => { + if (eq(w,z) || eq(w,neg(z))) return true + const ti = times(z, complex(zero(z.re), one(z.im))) + return eq(w,ti) || eq(w,neg(ti)) + } +} diff --git a/src/complex/native.mjs b/src/complex/native.mjs index bea1db8..4f63c8f 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -3,6 +3,7 @@ export * from './Types/Complex.mjs' export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' +export {associate} from './associate.mjs' export {conjugate} from './conjugate.mjs' export {complex} from './complex.mjs' export {equal} from './equal.mjs' diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index f6abd23..04948df 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -3,6 +3,7 @@ import {reducingOperation} from './reducingOperation.mjs' export * from './Types/generic.mjs' export const add = reducingOperation +export const gcd = reducingOperation export {lcm} from './lcm.mjs' export {mean} from './mean.mjs' export {mod} from './mod.mjs' diff --git a/src/generic/lcm.mjs b/src/generic/lcm.mjs index adc3dfb..04e78b5 100644 --- a/src/generic/lcm.mjs +++ b/src/generic/lcm.mjs @@ -1,3 +1,5 @@ +import {reducingOperation} from './reducingOperation.mjs' + export const lcm = { 'T,T': ({ 'multiply(T,T)': multT, @@ -5,3 +7,4 @@ export const lcm = { 'gcd(T,T)': gcdT }) => (a,b) => multT(quotT(a, gcdT(a,b)), b) } +Object.assign(lcm, reducingOperation) diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 18643b0..1d7d1e9 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -103,4 +103,13 @@ describe('The default full pocomath instance "math"', () => { assert.strictEqual(math.choose(21n, 2n), 210n) }) + it('calculates multi-way gcds and lcms', () => { + assert.strictEqual(math.gcd(30,105,42), 3) + assert.ok( + math.associate( + math.lcm( + math.complex(2n,1n), math.complex(1n,1n), math.complex(0n,1n)), + math.complex(1n,3n))) + }) + }) -- 2.34.1 From 5dab7d64e770cf522dee2cffaf04f7c150eff9eb Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 09:35:11 -0700 Subject: [PATCH 03/10] fix(templates): Enhance the catchall to call the correct specialization Also enhances type inference in preparation for template types. --- src/complex/native.mjs | 2 +- src/core/PocomathInstance.mjs | 193 ++++++++++++++++++++++++++-------- test/custom.mjs | 22 ++++ 3 files changed, 174 insertions(+), 43 deletions(-) diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 4f63c8f..1dfb45e 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -4,8 +4,8 @@ export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' export {associate} from './associate.mjs' -export {conjugate} from './conjugate.mjs' export {complex} from './complex.mjs' +export {conjugate} from './conjugate.mjs' export {equal} from './equal.mjs' export {gcd} from './gcd.mjs' export {invert} from './invert.mjs' diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 6fab6c9..a698488 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -27,6 +27,7 @@ export default class PocomathInstance { 'importDependencies', 'install', 'installType', + 'joinTypes', 'name', 'typeOf', 'Types', @@ -293,7 +294,20 @@ export default class PocomathInstance { } this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec + this._subtypes[type] = new Set() this._priorTypes[type] = new Set() + // Update all the subtype sets of supertypes up the chain, and + // while we are at it add trivial conversions from subtypes to supertypes + // to help typed-function match signatures properly: + let nextSuper = spec.refines + while (nextSuper) { + this._typed.addConversion( + {from: type, to: nextSuper, convert: x => x}) + this._invalidateDependents(':' + nextSuper) + this._priorTypes[nextSuper].add(type) + this._subtypes[nextSuper].add(type) + nextSuper = this.Types[nextSuper].refines + } /* Now add conversions to this type */ for (const from in (spec.from || {})) { if (from in this.Types) { @@ -304,50 +318,81 @@ export default class PocomathInstance { {from, to: nextSuper, convert: spec.from[from]}) this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(from) + /* And all of the subtypes of from are now prior as well: */ + for (const subtype of this._subtypes[from]) { + this._priorTypes[nextSuper].add(subtype) + } nextSuper = this.Types[nextSuper].refines } } } /* And add conversions from this type */ for (const to in this.Types) { - if (type in (this.Types[to].from || {})) { - if (spec.refines == to || spec.refines in this._subtypes[to]) { - throw new SyntaxError( - `Conversion of ${type} to its supertype ${to} disallowed.`) - } - let nextSuper = to - while (nextSuper) { - this._typed.addConversion({ - from: type, - to: nextSuper, - convert: this.Types[to].from[type] - }) - this._invalidateDependents(':' + nextSuper) - this._priorTypes[nextSuper].add(type) - nextSuper = this.Types[nextSuper].refines + 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]) { + throw new SyntaxError( + `Conversion of ${type} to its supertype ${to} disallowed.`) + } + let nextSuper = to + while (nextSuper) { + this._typed.addConversion({ + from: type, + to: nextSuper, + convert: this.Types[to].from[fromtype] + }) + this._invalidateDependents(':' + nextSuper) + this._priorTypes[nextSuper].add(type) + nextSuper = this.Types[nextSuper].refines + } } } } - // Update all the subtype sets of supertypes up the chain, and - // while we are at it add trivial conversions from subtypes to supertypes - // to help typed-function match signatures properly: - this._subtypes[type] = new Set() - let nextSuper = spec.refines - while (nextSuper) { - this._typed.addConversion( - {from: type, to: nextSuper, convert: x => x}) - this._invalidateDependents(':' + nextSuper) - this._priorTypes[nextSuper].add(type) - this._subtypes[nextSuper].add(type) - nextSuper = this.Types[nextSuper].refines - } - // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => () => type} + console.log('Adding', type, 'to typeOf') 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 + * argument `convert` is true, a convertible type is considered a + * a subtype (defaults to false). + */ + joinTypes(types, convert) { + let join = '' + for (const type of types) { + join = this._joinTypes(join, type, convert) + } + return join + } + /* helper for above */ + _joinTypes(typeA, typeB, convert) { + if (!typeA) return typeB + if (!typeB) return typeA + if (typeA === typeB) return typeA + const subber = convert ? this._priorTypes : this._subtypes + if (subber[typeB].has(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 + 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 + nextSuper = this.Types[nextSuper].refines + } + } + return 'any' + } /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ @@ -530,30 +575,94 @@ export default class PocomathInstance { const signature = substituteInSig( trimSignature, theTemplateParam, 'any') /* The catchall signature has to detect the actual type of the call - * and add the new instantiations + * and add the new instantiations. We should really be using the + * typed-function parser to do the manipulations below, but we don't + * have access. The firs section prepares the type inference data: */ - const argTypes = trimSignature.split(',') - let exemplar = -1 - for (let i = 0; i < argTypes.length; ++i) { - const argType = argTypes[i].trim() - if (argType === theTemplateParam) { - exemplar = i - break + const parTypes = trimSignature.split(',') + const inferences = [] + const typer = entity => this.typeOf(entity) + let ambiguous = true + for (let parType of parTypes) { + parType = parType.trim() + if (parType.slice(0,3) === '...') { + parType = parType.slice(3).trim() + } + if (parType === theTemplateParam) { + inferences.push(typer) + ambiguous = false + } else { + inferences.push(false) } } - if (exemplar < 0) { + if (ambiguous) { throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } + /* Now build the catchall implementation */ const self = this const patch = (refs) => (...args) => { - const example = args[exemplar] - const instantiateFor = self.typeOf(example) + /* First 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 || argType === 'any') { + throw TypeError( + `Type inference failed for argument ${j} of ${name}`) + } + 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') { + usedConversions = true + instantiateFor = self.joinTypes(argTypes, usedConversions) + if (instantiateFor === 'any') { + throw TypeError('No common type for arguments to ' + name) + } + } + /* 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 + const wantType = substituteInSig( + parTypes[i], theTemplateParam, instantiateFor) + if (wantType !== parTypes[i]) { + args[j] = self._typed.convert(args[j], wantType) + } + } + } + /* Arrange that the desired instantiation will be there next + * time so we don't have to go through that again for this type + */ refs[theTemplateParam] = instantiateFor behavior.instantiations.add(instantiateFor) self._invalidate(name) - // And for now, we have to rely on the "any" implementation. Hope - // it matches the instantiated one! + // And update refs because we now know the type we're instantiating + // for: + for (const dep of behavior.uses) { + let [func, needsig] = dep.split(/[()]/) + if (needsig && self._typed.isTypedFunction(refs[dep])) { + const subsig = substituteInSig( + needsig, theTemplateParam, instantiateFor) + if (subsig !== needsig) { + refs[dep] = self._typed.find(refs[dep], subsig) + } + } + } + // Finally ready to make the call. return behavior.does(refs)(...args) } this._addTFimplementation( diff --git a/test/custom.mjs b/test/custom.mjs index 9fb66b1..76bb88a 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -8,6 +8,7 @@ import * as complex from '../src/complex/all.mjs' import * as complexAdd from '../src/complex/add.mjs' import * as complexNegate from '../src/complex/negate.mjs' import * as complexComplex from '../src/complex/complex.mjs' +import * as bigintAdd from '../src/bigint/add.mjs' import * as concreteSubtract from '../src/generic/subtract.concrete.mjs' import * as genericSubtract from '../src/generic/subtract.mjs' import extendToComplex from '../src/complex/extendToComplex.mjs' @@ -112,4 +113,25 @@ describe('A custom instance', () => { math.complex(1n, -3n)) }) + it("instantiates templates correctly", () => { + const inst = new PocomathInstance('InstantiateTemplates') + inst.install(numberAdd) + inst.install({typeMerge: {'T,T': ({T}) => (t,u) => 'Merge to ' + T }}) + assert.strictEqual(inst.typeMerge(7,6.28), 'Merge to number') + assert.strictEqual(inst.typeMerge(7,6), 'Merge to NumInt') + assert.strictEqual(inst.typeMerge(7.35,6), 'Merge to number') + inst.install(complexAdd) + inst.install(complexComplex) + inst.install(bigintAdd) + assert.strictEqual( + inst.typeMerge(6n, inst.complex(3n, 2n)), + 'Merge to GaussianInteger') + assert.strictEqual( + inst.typeMerge(3, inst.complex(4.5,2.1)), + 'Merge to Complex') + // The following is the current behavior, since both `3+0i` and `3n + 0ni` + // are Complex, but it is unfortunate and hopefully it will be fixed + // with templates: + assert.strictEqual(inst.typeMerge(3, 3n), 'Merge to Complex') + }) }) -- 2.34.1 From 01658b13c2dfb6d274b25dc5d7f875fc6fb13cc5 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 09:42:13 -0700 Subject: [PATCH 04/10] test(templates): Add one more (unfortunate) test case --- test/custom.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/custom.mjs b/test/custom.mjs index 76bb88a..a5eece7 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -129,9 +129,12 @@ 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 both `3+0i` and `3n + 0ni` - // are Complex, but it is unfortunate and hopefully it will be fixed - // with templates: + // The following is the current behavior, since 3 converts to 3+0i + // and 3n converts to 3n+0ni, both of which are technically Complex. + // This will remain the case even with templated Complex, because + // both Complex and Complex will refine Complex (for the + // sake of catching new specializations). Not sure whether that will be + // OK or a problem that will have to be dealt with. assert.strictEqual(inst.typeMerge(3, 3n), 'Merge to Complex') }) }) -- 2.34.1 From 21ce098f98b3d0ea12f9b23c22ac838e173813cc Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 10:20:34 -0700 Subject: [PATCH 05/10] test(templates): Check that unjoinable types are detected --- test/custom.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/custom.mjs b/test/custom.mjs index a5eece7..d232dd5 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -136,5 +136,10 @@ describe('A custom instance', () => { // sake of catching new specializations). Not sure whether that will be // OK or a problem that will have to be dealt with. assert.strictEqual(inst.typeMerge(3, 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... + inst.install(genericSubtract) + assert.throws(() => inst.typeMerge(3, undefined), TypeError) }) }) -- 2.34.1 From 27bf23db544d5aa74d319ab0da9a7ea3d1b442fc Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 11:27:40 -0700 Subject: [PATCH 06/10] feat(Tuple): Stub for a template type This adds the target initial definition of a homogeneous Tuple type, and just enough processing that the type can be defined and non-template methods that deal with the generic base type Tuple can be called. Still has no actual template instantiation. --- src/core/PocomathInstance.mjs | 29 ++++++++++++- src/pocomath.mjs | 10 ++++- src/tuple/Types/Tuple.mjs | 82 +++++++++++++++++++++++++++++++++++ src/tuple/equal.mjs | 14 ++++++ src/tuple/isZero.mjs | 6 +++ src/tuple/length.mjs | 3 ++ src/tuple/native.mjs | 20 +++++++++ src/tuple/tuple.mjs | 6 +++ test/tuple/_native.mjs | 9 ++++ 9 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 src/tuple/Types/Tuple.mjs create mode 100644 src/tuple/equal.mjs create mode 100644 src/tuple/isZero.mjs create mode 100644 src/tuple/length.mjs create mode 100644 src/tuple/native.mjs create mode 100644 src/tuple/tuple.mjs create mode 100644 test/tuple/_native.mjs diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index a698488..22720d0 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -45,6 +45,8 @@ export default class PocomathInstance { */ this.Types = {any: anySpec} this.Types[theTemplateParam] = anySpec + // All the template types that have been defined + this._templateTypes = {} this._subtypes = {} // For each type, gives all of its (in)direct 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. @@ -263,10 +265,15 @@ export default class PocomathInstance { * the corresponding changes to the _typed object immediately */ installType(type, spec) { - if (this._templateParam(type)) { + const parts = type.split(/[<,>]/) + if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) } + if (parts.some(this._templateParam.bind(this))) { + // It's a template, deal with it separately + return this._installTemplateType(type, spec) + } if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) @@ -353,7 +360,6 @@ export default class PocomathInstance { // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => () => type} - console.log('Adding', type, 'to typeOf') this._installFunctions({typeOf: imp}) } @@ -393,6 +399,7 @@ export default class PocomathInstance { } return 'any' } + /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ @@ -400,6 +407,23 @@ export default class PocomathInstance { return Array.from(this._usedTypes).filter(t => !(t in this.Types)) } + /* Used internally to install a template type */ + _installTemplateType(type, spec) { + const base = type.split('<')[0] + /* For now, just allow a single template per base type; that + * might need to change later: + */ + if (base in this._templateTypes) { + if (spec !== this._templateTypes[base].spec) { + throw new SyntaxError( + `Conflicting definitions of template type ${type}`) + } + return + } + // Nothing actually happens until we match a template parameter + this._templateTypes[base] = {type, spec} + } + /* Used internally by install, see the documentation there */ _installFunctions(functions) { for (const [name, spec] of Object.entries(functions)) { @@ -509,6 +533,7 @@ export default class PocomathInstance { `Every implementation for ${name} uses an undefined type;\n` + ` signatures: ${Object.keys(imps)}`) } + // Mark this method as being in the midst of being reassembled Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} for (const [rawSignature, behavior] of usableEntries) { diff --git a/src/pocomath.mjs b/src/pocomath.mjs index dee980e..9081283 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -3,10 +3,18 @@ import PocomathInstance from './core/PocomathInstance.mjs' import * as numbers from './number/native.mjs' import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' +import * as tuple from './tuple/native.mjs' +// Most of tuple is not ready yet: +const tupleReady = { + Tuple: tuple.Tuple, + equal: tuple.equal, + length: tuple.length, + tuple: tuple.tuple +} import * as generic from './generic/all.mjs' import * as ops from './ops/all.mjs' const math = PocomathInstance.merge( - 'math', numbers, bigints, complex, generic, ops) + 'math', numbers, bigints, complex, tupleReady, generic, ops) export default math diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs new file mode 100644 index 0000000..a6dab25 --- /dev/null +++ b/src/tuple/Types/Tuple.mjs @@ -0,0 +1,82 @@ +/* A template type representing a homeogeneous tuple of elements */ +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', { + // For now we will assume 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 => { + return 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 + 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: + from: { + 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), + // Here since there is no U it's a straight conversion: + T: t => ({elts: [u]}), // singleton promotion + // Whereas the following will let you go directly from an element + // convertible to T to a singleton Tuple. Not sure if we really + // want that, but we'll try it just for kicks. + U: convert => u => ({elts: [convert(u)]}) + } +}) + +Tuple.promoteUnary = { + 'Tuple': ({'self(T)': me}) => t => ({elts: t.elts.map(me)}) +} + +Tuple.promoteBinaryUnary = { + 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU}) => (s,t) => { + let i = -1 + let result = [] + while (true) { + i += 1 + if (i < s.elts.length) { + if (i < t.elts.length) result.append(meB(s.elts[i], t.elts[i])) + else results.append(meU(s.elts[i])) + continue + } + if (i < t.elts.length) result.append(meU(t.elts[i])) + else break + } + return {elts: result} + } +} + +Tuple.promoteBinary = { + 'Tuple,Tuple': ({'self(T,T)': meB}) => (s,t) => { + const lim = Math.max(s.elts.length, t.elts.length) + const result = [] + for (let i = 0; i < lim; ++i) { + result.append(meB(s.elts[i], t.elts[i])) + } + return {elts: result} + } +} + +Tuple.promoteBinaryStrict = { + 'Tuple,Tuple': ({'self(T,T)': meB}) => (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.append(meB(s.elts[i], t.elts[i])) + } + return {elts: result} + } +} + +export {Tuple} diff --git a/src/tuple/equal.mjs b/src/tuple/equal.mjs new file mode 100644 index 0000000..8186624 --- /dev/null +++ b/src/tuple/equal.mjs @@ -0,0 +1,14 @@ +export * from './Types/Tuple.mjs' + +export const equal = { + // Change this to a template implementation (or maybe add template + // implementation to handle matching types, and have mixed template-base + // method returning false to catch test of two tuples of different types. + 'Tuple,Tuple': ({self, length}) => (s,t) => { + if (length(s) !== length(t)) return false + for (let i = 0; i < length(s); ++i) { + if (!self(s.elts[i], t.elts[i])) return false + } + return true + } +} diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs new file mode 100644 index 0000000..1c3d9d2 --- /dev/null +++ b/src/tuple/isZero.mjs @@ -0,0 +1,6 @@ +export {Tuple} from './Types/Tuple.mjs' + +export const isZero = { + 'Tuple': ({'self(T)': me}) => t => t.elts.every(isZero) +} + diff --git a/src/tuple/length.mjs b/src/tuple/length.mjs new file mode 100644 index 0000000..f3e8f2d --- /dev/null +++ b/src/tuple/length.mjs @@ -0,0 +1,3 @@ +export {Tuple} from './Types/Tuple.mjs' + +export const length = {Tuple: () => t => t.elts.length} diff --git a/src/tuple/native.mjs b/src/tuple/native.mjs new file mode 100644 index 0000000..fcf6f0d --- /dev/null +++ b/src/tuple/native.mjs @@ -0,0 +1,20 @@ +import {Tuple} from './Types/Tuple.mjs' + +export const add = Tuple.promoteBinaryUnary +export const complex = Tuple.promoteBinaryStrict +export const conjugate = Tuple.promoteUnary +// May want to replace equal with compare based on lexicographic ordering? +export {equal} from './equal.mjs' +export const invert = Tuple.promoteUnary +export {isZero} from './isZero.mjs' +export {length} from './length.mjs' +export const multiply = Tuple.promoteBinaryUnary +export const negate = Tuple.promoteUnary +export const one = Tuple.promoteUnary +export const quotient = Tuple.promoteBinaryStrict +export const roundquotient = Tuple.promoteBinaryStrict +export const sqrt = Tuple.promoteUnary +export {tuple} from './tuple.mjs' +export const zero = Tuple.promoteUnary + +export {Tuple} diff --git a/src/tuple/tuple.mjs b/src/tuple/tuple.mjs new file mode 100644 index 0000000..a025028 --- /dev/null +++ b/src/tuple/tuple.mjs @@ -0,0 +1,6 @@ +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 = {'...any': () => args => ({elts: args})} diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs new file mode 100644 index 0000000..3268cf6 --- /dev/null +++ b/test/tuple/_native.mjs @@ -0,0 +1,9 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' + +describe('tuple', () => { + it('can be created and provide its length', () => { + assert.strictEqual(math.length(math.tuple(3,5.2,2n)), 3) + }) + +}) -- 2.34.1 From e82bcf5a9c7b8ce0f51114211f4afda6775939be Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 17:55:53 -0700 Subject: [PATCH 07/10] refactor: Include more code that should work for instantiating type templates In particular, there is now an empty stub for the function that actually installs the instantiations into the PocomathInstance --- src/core/PocomathInstance.mjs | 100 ++++++++++++------ src/core/dependencyExtractor.mjs | 12 --- src/core/extractors.mjs | 41 +++++++ ...yExtractor.mjs => dependencyExtractor.mjs} | 2 +- 4 files changed, 110 insertions(+), 45 deletions(-) delete mode 100644 src/core/dependencyExtractor.mjs create mode 100644 src/core/extractors.mjs rename test/core/{_dependencyExtractor.mjs => dependencyExtractor.mjs} (89%) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 22720d0..1fce6b3 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,8 +1,8 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' -import dependencyExtractor from './dependencyExtractor.mjs' +import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' -import {subsetOfKeys, typesOfSignature} from './utils.mjs' +import {typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -461,9 +461,12 @@ export default class PocomathInstance { this._addAffect(depname, name) } for (const type of typesOfSignature(signature)) { - if (this._templateParam(type)) continue - this._usedTypes.add(type) - this._addAffect(':' + type, name) + for (const word of type.split(/[<>]/)) { + if (word.length == 0) continue + if (this._templateParam(word)) continue + this._usedTypes.add(word) + this._addAffect(':' + word, name) + } } } } @@ -526,23 +529,38 @@ export default class PocomathInstance { if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } - const usableEntries = Object.entries(imps).filter( - ([signature]) => subsetOfKeys(typesOfSignature(signature), this.Types)) + /* Collect the entries we know the types for */ + const usableEntries = [] + for (const entry of Object.entries(imps)) { + let keep = true + for (const type of typesOfSignature(entry[0])) { + if (type in this.Types) continue + const baseType = type.split('<')[0] + if (baseType in this._templateTypes) continue + keep = false + break + } + if (keep) usableEntries.push(entry) + } if (usableEntries.length === 0) { throw new SyntaxError( `Every implementation for ${name} uses an undefined type;\n` + ` signatures: ${Object.keys(imps)}`) } - // Mark this method as being in the midst of being reassembled + /* Initial error checking done; mark this method as being + * 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)) { - if (this._templateParam(type)) { // template types need better check - explicit = false - break + for (const word of type.split(/[<>]/)) { + if (this._templateParam(word)) { + explicit = false + break + } } } if (explicit) { @@ -591,36 +609,31 @@ export default class PocomathInstance { innerRefs[dep] = refs[outerName] } } - const original = behavior.does(innerRefs) return behavior.does(innerRefs) } this._addTFimplementation(tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ + let templateCall = `<${theTemplateParam}>` + /* Relying here that the base of 'Foo' is 'Foo': */ + let baseSignature = substituteInSig(trimSignature, templateCall, '') + /* Any remaining template params are top-level */ const signature = substituteInSig( - trimSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, 'any') /* The catchall signature has to detect the actual type of the call - * and add the new instantiations. We should really be using the - * typed-function parser to do the manipulations below, but we don't - * have access. The firs section prepares the type inference data: + * and add the new instantiations. + * First, prepare the type inference data: */ const parTypes = trimSignature.split(',') - const inferences = [] - const typer = entity => this.typeOf(entity) - let ambiguous = true - for (let parType of parTypes) { - parType = parType.trim() - if (parType.slice(0,3) === '...') { - parType = parType.slice(3).trim() - } - if (parType === theTemplateParam) { - inferences.push(typer) - ambiguous = false - } else { - inferences.push(false) - } - } - if (ambiguous) { + const topTyper = entity => this.typeOf(entity) + const inferences = parTypes.map( + type => generateTypeExtractor( + type, + theTemplateParam, + topTyper, + this.joinTypes.bind(this), + this._templateTypes)) + if (inferences.every(x => !x)) { // all false throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } @@ -654,9 +667,24 @@ export default class PocomathInstance { usedConversions = true instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { + // Need a more informative error message here throw TypeError('No common type for arguments to ' + name) } } + /* Generate the list of actual wanted types */ + const wantTypes = parTypes.map(type => substituteInSig( + type, theTemplateParam, instantiateFor)) + /* Now we have to add any actual types that are relevant + * to this invocation. Namely, that would be every formal parameter + * type in the invocation, with the parameter template instantiated + * by instantiateFor, and for all of instantiateFor's "prior types" + */ + for (j = 0; j < parTypes.length; ++j) { + if (wantTypes[i] !== parTypes[i] && wantTypes.includes('<')) { + // actually used the param and is a template + self._ensureTemplateTypes(parTypes[i], instantiateFor) + } + } /* Transform the arguments if we used any conversions: */ if (usedConversions) { i = - 1 @@ -774,4 +802,12 @@ export default class PocomathInstance { } imps[signature] = does(refs) } + + /* HERE!! This function needs to analyze the template and make sure the + * instantiations of it for type and all prior types of type are present + * in the instance + */ + _ensureTemplateTypes(template, type) { + } + } diff --git a/src/core/dependencyExtractor.mjs b/src/core/dependencyExtractor.mjs deleted file mode 100644 index 1b1091c..0000000 --- a/src/core/dependencyExtractor.mjs +++ /dev/null @@ -1,12 +0,0 @@ -/* Call this with an empty Set object S, and it returns an entity E - * from which properties can be extracted, and at any time S will - * contain all of the property names that have been extracted from E. - */ -export default function dependencyExtractor(destinationSet) { - return new Proxy({}, { - get: (target, property) => { - destinationSet.add(property) - return {} - } - }) -} diff --git a/src/core/extractors.mjs b/src/core/extractors.mjs new file mode 100644 index 0000000..ff63d9d --- /dev/null +++ b/src/core/extractors.mjs @@ -0,0 +1,41 @@ +/* Call this with an empty Set object S, and it returns an entity E + * from which properties can be extracted, and at any time S will + * contain all of the property names that have been extracted from E. + */ +export function dependencyExtractor(destinationSet) { + return new Proxy({}, { + get: (target, property) => { + destinationSet.add(property) + return {} + } + }) +} + +/* Given a (template) type name, what the template parameter is, + * a top level typer, and a library of templates, + * produces a function that will extract the instantantion type from an + * instance. Currently relies heavily on there being only unary templates. + * + * We should really be using the typed-function parser to do the + * manipulations below, but at the moment we don't have access. + */ +export function generateTypeExtractor( + type, param, topTyper, typeJoiner, templates) +{ + type = type.trim() + if (type.slice(0,3) === '...') { + type = type.slice(3).trim() + } + if (type === param) return topTyper + if (!(type.includes('<'))) return false // no template type to extract + const base = type.split('<',1)[0] + if (!(base in templates)) return false // unknown template + const arg = type.slice(base.length+1, -1) + const argExtractor = generateTypeExtractor( + arg, param, topTyper, typeJointer, templates) + if (!argExtractor) return false + return templates[base].infer({ + typeOf: argExtractor, + joinTypes: typeJoiner + }) +} diff --git a/test/core/_dependencyExtractor.mjs b/test/core/dependencyExtractor.mjs similarity index 89% rename from test/core/_dependencyExtractor.mjs rename to test/core/dependencyExtractor.mjs index 91e0e40..bc1683f 100644 --- a/test/core/_dependencyExtractor.mjs +++ b/test/core/dependencyExtractor.mjs @@ -1,5 +1,5 @@ import assert from 'assert' -import dependencyExtractor from '../../src/core/dependencyExtractor.mjs' +import {dependencyExtractor} from '../../src/core/extractors.mjs' describe('dependencyExtractor', () => { it('will record the keys of a destructuring function', () => { -- 2.34.1 From a7433371341d487744a717afe6ece049d2635db2 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 3 Aug 2022 19:57:11 -0700 Subject: [PATCH 08/10] refactor: fill in stub for instantiating template type --- src/core/PocomathInstance.mjs | 85 ++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 1fce6b3..0253de0 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -7,6 +7,7 @@ import {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 templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type @@ -803,11 +804,91 @@ export default class PocomathInstance { imps[signature] = does(refs) } - /* HERE!! This function needs to analyze the template and make sure the + /* This function analyzes the template and makes sure the * instantiations of it for type and all prior types of type are present - * in the instance + * in the instance. */ _ensureTemplateTypes(template, type) { + let [base, arg] = template.split('<', 2) + arg = arg.slice(0,-1) + if (!arg) { + throw new Error( + 'Implementation error in _ensureTemplateTypes', template, type) + } + let instantiations + if (this._templateParam(arg)) { // 1st-level template + instantiations = new Set(this._priorTypes(type)) + instantiations.add(type) + } else { // nested template + instantiations = this._ensureTemplateTypes(arg, type) + } + const resultingTypes = new Set() + for (const iType of instantiations) { + const resultType = this._maybeAddTemplateType(base, iType) + if (resultType) resultingTypes.push(resultType) + } + 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. + * Returns the name of the type if added, false otherwise. + */ + _maybeAddTemplateType(base, instantiator) { + const wantsType = `${base}<${instantiator}>` + 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 = {} + const template = this._templateTypes[base] + if (!template) { + throw new Error( + `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) + } + const instantiatorSpec = this.Types[instantiator] + if (instantiatorSpec.refines) { + // Assuming all templates are covariant, for now + newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` + } + let beforeTypes = [] + if (instantiatorSpec.before) { + beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) + } + if (template.before) { + for (const beforeTmpl of template.before) { + beforeTypes.push( + substituteInSig(beforeTmpl, theTemplateParam, instantiator)) + } + } + if (beforeTypes.length > 0) { + newTypeSpec.before = beforeTypes + } + newTypeSpec.test = template.test(instantiatorSpec.test) + if (template.from) { + newTypeSpec.from = {} + for (let source in template.from) { + source = substituteInSig(source, theTemplateParam, instantiator) + const usesFromParam = false + for (const word of source.split(/[<>]/)) { + if (word === templateFromParam) { + usesFromParam = true + break + } + } + if (usesFromParam) { + for (const iFrom in instantiatorSpec.from) { + const finalSource = substituteInSig( + source, templateFromParam, iFrom) + newTypeSpec[finalSource] = template.from[source]( + instantiatorSpec.from[iFrom]) + } + } else { + newTypeSpec[source] = template.from[source] + } + } + } + this.installType(wantsType, newTypeSpec) + return wantsType } } -- 2.34.1 From b0fb00422434311a5d1130c72eb24d891a35e7f1 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Thu, 4 Aug 2022 08:23:14 -0700 Subject: [PATCH 09/10] feat: Template types for Pocomath Tuple and a couple of functions on it are now working according to the target spec. As desired, no instantiations of a template type are made until some function that takes an instantiation is called. --- src/core/PocomathInstance.mjs | 194 ++++++++++++++++++++++++++++------ src/core/extractors.mjs | 4 +- src/pocomath.mjs | 1 + src/tuple/Types/Tuple.mjs | 6 +- src/tuple/isZero.mjs | 6 +- src/tuple/tuple.mjs | 2 +- test/tuple/_native.mjs | 21 +++- 7 files changed, 190 insertions(+), 44 deletions(-) diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 0253de0..cc999c1 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -184,6 +184,9 @@ export default class PocomathInstance { if (type === 'any' || this._templateParam(type)) continue this.installType(type, spec) } + for (const [base, info] of Object.entries(other._templateTypes)) { + this._installTemplateType(info.type, info.spec) + } const migrateImps = {} for (const operator in other._imps) { if (operator != 'typeOf') { // skip the builtin, we already have it @@ -380,6 +383,7 @@ export default class PocomathInstance { _joinTypes(typeA, typeB, convert) { if (!typeA) return typeB if (!typeB) return typeA + if (typeA === 'any' || typeB === 'any') return 'any' if (typeA === typeB) return typeA const subber = convert ? this._priorTypes : this._subtypes if (subber[typeB].has(typeA)) return typeB @@ -612,12 +616,13 @@ export default class PocomathInstance { } return behavior.does(innerRefs) } - this._addTFimplementation(tf_imps, signature, {uses, does: patch}) + this._addTFimplementation( + tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = substituteInSig(trimSignature, templateCall, '') + let baseSignature = trimSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSig( baseSignature, theTemplateParam, 'any') @@ -626,6 +631,7 @@ export default class PocomathInstance { * First, prepare the type inference data: */ const parTypes = trimSignature.split(',') + const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') const topTyper = entity => this.typeOf(entity) const inferences = parTypes.map( type => generateTypeExtractor( @@ -638,10 +644,31 @@ export default class PocomathInstance { throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } + /* And eliminate template parameters from the dependencies */ + const simplifiedUses = {} + for (const dep of behavior.uses) { + let [func, needsig] = dep.split(/[()]/) + if (needsig) { + const subsig = substituteInSig(needsig, theTemplateParam, '') + if (subsig === needsig) { + simplifiedUses[dep] = dep + } else { + simplifiedUses[dep] = func + } + } else { + simplifiedUses[dep] = dep + } + } /* Now build the catchall implementation */ const self = this const patch = (refs) => (...args) => { - /* First infer the type we actually should have been called for */ + /* 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 */ @@ -652,10 +679,15 @@ export default class PocomathInstance { if (i < inferences.length - 1) ++i if (inferences[i]) { const argType = inferences[i](arg) - if (!argType || argType === 'any') { + 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(', ')) + } argTypes.push(argType) } } @@ -669,7 +701,11 @@ export default class PocomathInstance { instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { // Need a more informative error message here - throw TypeError('No common type for arguments to ' + name) + 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') } } /* Generate the list of actual wanted types */ @@ -681,9 +717,9 @@ export default class PocomathInstance { * by instantiateFor, and for all of instantiateFor's "prior types" */ for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[i] !== parTypes[i] && wantTypes.includes('<')) { + if (wantTypes[j] !== parTypes[j] && wantTypes[j].includes('<')) { // actually used the param and is a template - self._ensureTemplateTypes(parTypes[i], instantiateFor) + self._ensureTemplateTypes(parTypes[j], instantiateFor) } } /* Transform the arguments if we used any conversions: */ @@ -691,13 +727,23 @@ export default class PocomathInstance { i = - 1 for (j = 0; j < args.length; ++j) { if (i < parTypes.length - 1) ++i - const wantType = substituteInSig( - parTypes[i], theTemplateParam, instantiateFor) + let wantType = parTypes[i] + if (wantType.slice(0,3) === '...') { + wantType = wantType.slice(3) + } + wantType = substituteInSig( + wantType, theTemplateParam, instantiateFor) if (wantType !== parTypes[i]) { args[j] = self._typed.convert(args[j], wantType) } } } + /* 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 */ @@ -706,22 +752,32 @@ export default class PocomathInstance { self._invalidate(name) // And update refs because we now know the type we're instantiating // for: - for (const dep of behavior.uses) { - let [func, needsig] = dep.split(/[()]/) - if (needsig && self._typed.isTypedFunction(refs[dep])) { - const subsig = substituteInSig( - needsig, theTemplateParam, instantiateFor) - if (subsig !== needsig) { - refs[dep] = self._typed.find(refs[dep], subsig) + const innerRefs = {} + for (const dep in simplifiedUses) { + const simplifiedDep = simplifiedUses[dep] + if (dep === simplifiedDep) { + innerRefs[dep] = refs[dep] + } else { + let [func, needsig] = dep.split(/[()]/) + if (self._typed.isTypedFunction(refs[simplifiedDep])) { + const subsig = substituteInSig( + needsig, theTemplateParam, instantiateFor) + innerRefs[dep] = self._typed.find( + refs[simplifiedDep], subsig) + } else { + innerRefs[dep] = refs[simplifiedDep] } } } // Finally ready to make the call. - return behavior.does(refs)(...args) + return behavior.does(innerRefs)(...args) } + // The actual uses value needs to be a set: + const outerUses = new Set(Object.values(simplifiedUses)) this._addTFimplementation( - tf_imps, signature, {uses: behavior.uses, does: patch}) + tf_imps, signature, {uses: outerUses, does: patch}) } + this._correctPartialSelfRefs(tf_imps) const tf = this._typed(name, tf_imps) Object.defineProperty(this, name, {configurable: true, value: tf}) return tf @@ -742,9 +798,17 @@ export default class PocomathInstance { let part_self_references = [] for (const dep of uses) { let [func, needsig] = dep.split(/[()]/) - const needTypes = needsig ? typesOfSignature(needsig) : new Set() - /* For now, punt on template parameters */ - if (needTypes.has(theTemplateParam)) needsig = '' + /* Safety check that can perhaps be removed: + * Verify that the desired signature has been fully grounded: + */ + if (needsig) { + const trysig = substituteInSig(needsig, theTemplateParam, '') + if (trysig !== needsig) { + throw new Error( + 'Attempt to add a template implementation: ' + + `${signature} with dependency ${dep}`) + } + } if (func === 'self') { if (needsig) { if (full_self_referential) { @@ -791,17 +855,77 @@ export default class PocomathInstance { return } if (part_self_references.length) { - imps[signature] = this._typed.referTo( - ...part_self_references, (...impls) => { + /* There is an obstruction here. The list part_self_references + * might contain a signature that requires conversion for self to + * handle. But I advocated this not be allowed in typed.referTo, which + * made sense for human-written functions, but is unfortunate now. + * So we have to defer creating these and correct them later, at + * least until we can add an option to typed-function. + */ + imps[signature] = { + deferred: true, + builtRefs: refs, + sigDoes: does, + psr: part_self_references + } + return + } + imps[signature] = does(refs) + } + + _correctPartialSelfRefs(imps) { + for (const aSignature in imps) { + if (!(imps[aSignature].deferred)) continue + const part_self_references = imps[aSignature].psr + const corrected_self_references = [] + 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) + continue + } + // No exact match, have to try to get one that matches with + // subtypes since the whole conversion thing in typed-function + // is too complicated to reproduce + let foundSig = false + const typeList = typesOfSignature(neededSig) + for (const otherSig in imps) { + const otherTypeList = typesOfSignature(otherSig) + if (typeList.length !== otherTypeList.length) continue + const allMatch = true + for (let k = 0; k < typeList.length; ++k) { + if (this._subtypes[otherTypeList[k]].has(typeList[k])) { + continue + } + allMatch = false + break + } + if (allMatch) { + foundSig = otherSig + break + } + } + if (foundSig) { + corrected_self_references.push(foundSig) + } else { + throw new Error( + 'Implement inexact self-reference in typed-function for ' + + neededSig) + } + } + const refs = imps[aSignature].builtRefs + const does = imps[aSignature].sigDoes + imps[aSignature] = this._typed.referTo( + ...corrected_self_references, (...impls) => { for (let i = 0; i < part_self_references.length; ++i) { refs[`self(${part_self_references[i]})`] = impls[i] } return does(refs) } ) - return } - imps[signature] = does(refs) } /* This function analyzes the template and makes sure the @@ -817,7 +941,7 @@ export default class PocomathInstance { } let instantiations if (this._templateParam(arg)) { // 1st-level template - instantiations = new Set(this._priorTypes(type)) + instantiations = new Set(this._priorTypes[type]) instantiations.add(type) } else { // nested template instantiations = this._ensureTemplateTypes(arg, type) @@ -825,7 +949,7 @@ export default class PocomathInstance { const resultingTypes = new Set() for (const iType of instantiations) { const resultType = this._maybeAddTemplateType(base, iType) - if (resultType) resultingTypes.push(resultType) + if (resultType) resultingTypes.add(resultType) } return resultingTypes } @@ -840,7 +964,7 @@ export default class PocomathInstance { // OK, need to generate the type from the template // Set up refines, before, test, and from const newTypeSpec = {} - const template = this._templateTypes[base] + const template = this._templateTypes[base].spec if (!template) { throw new Error( `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) @@ -848,6 +972,7 @@ export default class PocomathInstance { const instantiatorSpec = this.Types[instantiator] if (instantiatorSpec.refines) { // Assuming all templates are covariant, for now + this._maybeAddTemplateType(base, instantiatorSpec.refines) newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` } let beforeTypes = [] @@ -867,9 +992,10 @@ export default class PocomathInstance { if (template.from) { newTypeSpec.from = {} for (let source in template.from) { - source = substituteInSig(source, theTemplateParam, instantiator) - const usesFromParam = false - for (const word of source.split(/[<>]/)) { + const instSource = substituteInSig( + source, theTemplateParam, instantiator) + let usesFromParam = false + for (const word of instSource.split(/[<>]/)) { if (word === templateFromParam) { usesFromParam = true break @@ -878,12 +1004,12 @@ export default class PocomathInstance { if (usesFromParam) { for (const iFrom in instantiatorSpec.from) { const finalSource = substituteInSig( - source, templateFromParam, iFrom) - newTypeSpec[finalSource] = template.from[source]( + instSource, templateFromParam, iFrom) + newTypeSpec.from[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } } else { - newTypeSpec[source] = template.from[source] + newTypeSpec.from[instSource] = template.from[source] } } } diff --git a/src/core/extractors.mjs b/src/core/extractors.mjs index ff63d9d..0db3c0f 100644 --- a/src/core/extractors.mjs +++ b/src/core/extractors.mjs @@ -32,9 +32,9 @@ export function generateTypeExtractor( if (!(base in templates)) return false // unknown template const arg = type.slice(base.length+1, -1) const argExtractor = generateTypeExtractor( - arg, param, topTyper, typeJointer, templates) + arg, param, topTyper, typeJoiner, templates) if (!argExtractor) return false - return templates[base].infer({ + return templates[base].spec.infer({ typeOf: argExtractor, joinTypes: typeJoiner }) diff --git a/src/pocomath.mjs b/src/pocomath.mjs index 9081283..53e646f 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -8,6 +8,7 @@ import * as tuple from './tuple/native.mjs' const tupleReady = { Tuple: tuple.Tuple, equal: tuple.equal, + isZero: tuple.isZero, length: tuple.length, tuple: tuple.tuple } diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index a6dab25..7b4ed35 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -8,13 +8,11 @@ Tuple.installType('Tuple', { }) // Now the template type that is the primary use of this Tuple.installType('Tuple', { - // For now we will assume that any 'Type' refines 'Type', so this is + // 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 => { - return joinTypes(t.elts.map(typeOf)) - }, + 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 diff --git a/src/tuple/isZero.mjs b/src/tuple/isZero.mjs index 1c3d9d2..9375277 100644 --- a/src/tuple/isZero.mjs +++ b/src/tuple/isZero.mjs @@ -1,6 +1,8 @@ export {Tuple} from './Types/Tuple.mjs' export const isZero = { - 'Tuple': ({'self(T)': me}) => t => t.elts.every(isZero) + 'Tuple': ({'self(T)': me}) => 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 a025028..893b54d 100644 --- a/src/tuple/tuple.mjs +++ b/src/tuple/tuple.mjs @@ -3,4 +3,4 @@ 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 = {'...any': () => args => ({elts: args})} +export const tuple = {'...T': () => args => ({elts: args})} diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 3268cf6..7727ac9 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -3,7 +3,26 @@ import math from '../../src/pocomath.mjs' describe('tuple', () => { it('can be created and provide its length', () => { - assert.strictEqual(math.length(math.tuple(3,5.2,2n)), 3) + assert.strictEqual(math.length(math.tuple(3, 5.2, 2)), 3) + }) + + 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)]}) + }) + + it('can be tested for zero', () => { + assert.strictEqual(math.isZero(math.tuple(0,1)), false) + assert.strictEqual(math.isZero(math.tuple(0n,0n,0n,0n)), true) + assert.strictEqual(math.isZero(math.tuple(0,0.001,0)), false) + assert.strictEqual(math.isZero(math.tuple(0,math.complex(0,0))), true) }) }) -- 2.34.1 From 7ec96860197f16ce58a4b18619f63202f6b0df02 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 5 Aug 2022 05:44:28 -0700 Subject: [PATCH 10/10] feat: Homogeneous typed Tuple class using templates --- src/bigint/native.mjs | 2 + src/complex/add.mjs | 8 +- src/complex/associate.mjs | 2 +- src/complex/complex.mjs | 3 +- src/complex/{equal.mjs => equalTT.mjs} | 2 +- src/complex/native.mjs | 2 +- src/core/PocomathInstance.mjs | 204 ++++++++++++++++--------- src/core/utils.mjs | 5 + src/generic/arithmetic.mjs | 1 + src/generic/identity.mjs | 3 + src/generic/relational.mjs | 19 ++- src/number/native.mjs | 2 + src/ops/floor.mjs | 2 +- src/pocomath.mjs | 10 +- src/tuple/Types/Tuple.mjs | 26 ++-- src/tuple/equal.mjs | 14 -- src/tuple/equalTT.mjs | 11 ++ src/tuple/native.mjs | 5 +- test/custom.mjs | 18 +-- test/tuple/_native.mjs | 88 ++++++++++- 20 files changed, 291 insertions(+), 136 deletions(-) rename src/complex/{equal.mjs => equalTT.mjs} (95%) create mode 100644 src/generic/identity.mjs delete mode 100644 src/tuple/equal.mjs create mode 100644 src/tuple/equalTT.mjs diff --git a/src/bigint/native.mjs b/src/bigint/native.mjs index dc12d9e..6458912 100644 --- a/src/bigint/native.mjs +++ b/src/bigint/native.mjs @@ -1,9 +1,11 @@ import gcdType from '../generic/gcdType.mjs' +import {identity} from '../generic/identity.mjs' export * from './Types/bigint.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' +export const conjugate = {bigint: () => identity} export {divide} from './divide.mjs' export const gcd = gcdType('bigint') export {isZero} from './isZero.mjs' diff --git a/src/complex/add.mjs b/src/complex/add.mjs index 1afd22f..0c178d6 100644 --- a/src/complex/add.mjs +++ b/src/complex/add.mjs @@ -7,16 +7,16 @@ export const add = { */ 'Complex,number': ({ 'self(number,number)': addNum, - 'complex(any,any)': cplx + 'complex(number,number)': cplx }) => (z,x) => cplx(addNum(z.re, x), z.im), 'Complex,bigint': ({ 'self(bigint,bigint)': addBigInt, - 'complex(any,any)': cplx + 'complex(bigint,bigint)': cplx }) => (z,x) => cplx(addBigInt(z.re, x), z.im), 'Complex,Complex': ({ self, - 'complex(any,any)': cplx - }) => (w,z) => cplx(self(w.re, z.re), self(w.im, z.im)) + complex + }) => (w,z) => complex(self(w.re, z.re), self(w.im, z.im)) } diff --git a/src/complex/associate.mjs b/src/complex/associate.mjs index c6fab9e..f3106b4 100644 --- a/src/complex/associate.mjs +++ b/src/complex/associate.mjs @@ -4,7 +4,7 @@ export * from './Types/Complex.mjs' export const associate = { 'Complex,Complex': ({ 'multiply(Complex,Complex)': times, - 'equal(Complex,Complex)': eq, + 'equalTT(Complex,Complex)': eq, zero, one, complex, diff --git a/src/complex/complex.mjs b/src/complex/complex.mjs index 58d3e2e..c34434c 100644 --- a/src/complex/complex.mjs +++ b/src/complex/complex.mjs @@ -9,7 +9,8 @@ export const complex = { 'undefined': () => u => u, 'undefined,any': () => (u, y) => u, 'any,undefined': () => (x, u) => u, - 'any,any': () => (x, y) => ({re: x, im: y}), + 'undefined,undefined': () => (u, v) => u, + 'T,T': () => (x, y) => ({re: x, im: y}), /* Take advantage of conversions in typed-function */ Complex: () => z => z } diff --git a/src/complex/equal.mjs b/src/complex/equalTT.mjs similarity index 95% rename from src/complex/equal.mjs rename to src/complex/equalTT.mjs index 4eca63f..43fe0d1 100644 --- a/src/complex/equal.mjs +++ b/src/complex/equalTT.mjs @@ -1,6 +1,6 @@ export * from './Types/Complex.mjs' -export const equal = { +export const equalTT = { 'Complex,number': ({ 'isZero(number)': isZ, 'self(number,number)': eqNum diff --git a/src/complex/native.mjs b/src/complex/native.mjs index 1dfb45e..420ce88 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -6,7 +6,7 @@ export {add} from './add.mjs' export {associate} from './associate.mjs' export {complex} from './complex.mjs' export {conjugate} from './conjugate.mjs' -export {equal} from './equal.mjs' +export {equalTT} from './equalTT.mjs' export {gcd} from './gcd.mjs' export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index cc999c1..05636d4 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {makeChain} from './Chain.mjs' -import {typesOfSignature, subsetOfKeys} from './utils.mjs' +import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -30,6 +30,8 @@ export default class PocomathInstance { 'installType', 'joinTypes', 'name', + 'self', + 'Templates', 'typeOf', 'Types', 'undefinedTypes' @@ -41,13 +43,17 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() + this._typed.addTypes([{name: 'ground', test: () => true}]) /* List of types installed in the instance. We start with just dummies * for the 'any' type and for type parameters: */ this.Types = {any: anySpec} this.Types[theTemplateParam] = anySpec + this.Types.ground = anySpec // All the template types that have been defined - this._templateTypes = {} + this.Templates = {} + // The actual type testing functions + this._typeTests = {} this._subtypes = {} // For each type, gives all of its (in)direct 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. @@ -56,8 +62,8 @@ export default class PocomathInstance { * might match. */ this._priorTypes = {} - this._usedTypes = new Set() // all types that have occurred in a signature - this._doomed = new Set() // for detecting circular reference + this._seenTypes = new Set() // all types that have occurred in a signature + 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, { @@ -72,6 +78,12 @@ export default class PocomathInstance { }) this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions + + this._installFunctions({ + typeOf: {ground: {uses: new Set(), does: () => () => 'any'}} + }) + + this.joinTypes = this.joinTypes.bind(this) } /** @@ -181,10 +193,10 @@ export default class PocomathInstance { _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { - if (type === 'any' || this._templateParam(type)) continue + if (spec === anySpec) continue this.installType(type, spec) } - for (const [base, info] of Object.entries(other._templateTypes)) { + for (const [base, info] of Object.entries(other.Templates)) { this._installTemplateType(info.type, info.spec) } const migrateImps = {} @@ -290,7 +302,7 @@ export default class PocomathInstance { } let beforeType = spec.refines if (!beforeType) { - beforeType = 'any' + beforeType = 'ground' for (const other of spec.before || []) { if (other in this.Types) { beforeType = other @@ -303,17 +315,14 @@ export default class PocomathInstance { const supertypeTest = this.Types[spec.refines].test testFn = entity => supertypeTest(entity) && spec.test(entity) } + this._typeTests[type] = testFn this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec this._subtypes[type] = new Set() this._priorTypes[type] = new Set() - // Update all the subtype sets of supertypes up the chain, and - // while we are at it add trivial conversions from subtypes to supertypes - // to help typed-function match signatures properly: + // Update all the subtype sets of supertypes up the chain let nextSuper = spec.refines while (nextSuper) { - this._typed.addConversion( - {from: type, to: nextSuper, convert: x => x}) this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) this._subtypes[nextSuper].add(type) @@ -325,6 +334,7 @@ export default class PocomathInstance { // add conversions from "from" to this one and all its supertypes: let nextSuper = type while (nextSuper) { + if (this._priorTypes[nextSuper].has(from)) break this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) this._invalidateDependents(':' + nextSuper) @@ -384,6 +394,7 @@ 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 if (subber[typeB].has(typeA)) return typeB @@ -409,7 +420,7 @@ export default class PocomathInstance { * signatures of operations, but which have not actually been installed: */ undefinedTypes() { - return Array.from(this._usedTypes).filter(t => !(t in this.Types)) + return Array.from(this._seenTypes).filter(t => !(t in this.Types)) } /* Used internally to install a template type */ @@ -418,15 +429,15 @@ export default class PocomathInstance { /* For now, just allow a single template per base type; that * might need to change later: */ - if (base in this._templateTypes) { - if (spec !== this._templateTypes[base].spec) { + if (base in this.Templates) { + if (spec !== this.Templates[base].spec) { throw new SyntaxError( `Conflicting definitions of template type ${type}`) } return } // Nothing actually happens until we match a template parameter - this._templateTypes[base] = {type, spec} + this.Templates[base] = {type, spec} } /* Used internally by install, see the documentation there */ @@ -469,7 +480,7 @@ export default class PocomathInstance { for (const word of type.split(/[<>]/)) { if (word.length == 0) continue if (this._templateParam(word)) continue - this._usedTypes.add(word) + this._seenTypes.add(word) this._addAffect(':' + word, name) } } @@ -497,20 +508,20 @@ export default class PocomathInstance { * and if it has no implementations so far, set them up. */ _invalidate(name) { - if (this._doomed.has(name)) { - /* In the midst of a circular invalidation, so do nothing */ - return - } + if (this._invalid.has(name)) return if (!(name in this._imps)) { this._imps[name] = {} } - this._doomed.add(name) + this._invalid.add(name) this._invalidateDependents(name) - this._doomed.delete(name) const self = this Object.defineProperty(this, name, { configurable: true, - get: () => self._bundle(name) + get: () => { + const result = self._bundle(name) + self._invalid.delete(name) + return result + } }) } @@ -541,7 +552,7 @@ export default class PocomathInstance { for (const type of typesOfSignature(entry[0])) { if (type in this.Types) continue const baseType = type.split('<')[0] - if (baseType in this._templateTypes) continue + if (baseType in this.Templates) continue keep = false break } @@ -578,16 +589,10 @@ export default class PocomathInstance { behavior.instantiations = new Set() } let instantiationSet = new Set() - let trimSignature = rawSignature - if (rawSignature.charAt(0) === '!') { - trimSignature = trimSignature.slice(1) - instantiationSet = this._usedTypes - } else { - for (const instType of behavior.instantiations) { - instantiationSet.add(instType) - for (const other of this._priorTypes[instType]) { - instantiationSet.add(other) - } + for (const instType of behavior.instantiations) { + instantiationSet.add(instType) + for (const other of this._priorTypes[instType]) { + instantiationSet.add(other) } } @@ -595,7 +600,7 @@ export default class PocomathInstance { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue const signature = - substituteInSig(trimSignature, theTemplateParam, instType) + substituteInSig(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ if (signature in imps) continue const uses = new Set() @@ -622,15 +627,15 @@ export default class PocomathInstance { /* Now add the catchall signature */ let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ - let baseSignature = trimSignature.replaceAll(templateCall, '') + let baseSignature = rawSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSig( - baseSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call * and add the new instantiations. * First, prepare the type inference data: */ - const parTypes = trimSignature.split(',') + const parTypes = rawSignature.split(',') const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') const topTyper = entity => this.typeOf(entity) const inferences = parTypes.map( @@ -639,7 +644,7 @@ export default class PocomathInstance { theTemplateParam, topTyper, this.joinTypes.bind(this), - this._templateTypes)) + this.Templates)) if (inferences.every(x => !x)) { // all false throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) @@ -700,7 +705,6 @@ export default class PocomathInstance { usedConversions = true instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { - // Need a more informative error message here throw TypeError( `In call to ${name}, no type unifies arguments ` + args.toString() + '; of types ' + argTypes.toString() @@ -717,7 +721,7 @@ export default class PocomathInstance { * by instantiateFor, and for all of instantiateFor's "prior types" */ for (j = 0; j < parTypes.length; ++j) { - if (wantTypes[j] !== parTypes[j] && wantTypes[j].includes('<')) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { // actually used the param and is a template self._ensureTemplateTypes(parTypes[j], instantiateFor) } @@ -762,8 +766,9 @@ export default class PocomathInstance { if (self._typed.isTypedFunction(refs[simplifiedDep])) { const subsig = substituteInSig( needsig, theTemplateParam, instantiateFor) - innerRefs[dep] = self._typed.find( - refs[simplifiedDep], subsig) + let resname = simplifiedDep + if (resname === 'self') resname = name + innerRefs[dep] = self._pocoresolve(resname, subsig) } else { innerRefs[dep] = refs[simplifiedDep] } @@ -841,7 +846,7 @@ export default class PocomathInstance { // can bundle up func, and grab its signature if need be let destination = this[func] if (needsig) { - destination = this._typed.find(destination, needsig) + destination = this._pocoresolve(func, needsig) } refs[dep] = destination } @@ -886,27 +891,10 @@ export default class PocomathInstance { corrected_self_references.push(neededSig) continue } - // No exact match, have to try to get one that matches with + // No exact match, try to get one that matches with // subtypes since the whole conversion thing in typed-function // is too complicated to reproduce - let foundSig = false - const typeList = typesOfSignature(neededSig) - for (const otherSig in imps) { - const otherTypeList = typesOfSignature(otherSig) - if (typeList.length !== otherTypeList.length) continue - const allMatch = true - for (let k = 0; k < typeList.length; ++k) { - if (this._subtypes[otherTypeList[k]].has(typeList[k])) { - continue - } - allMatch = false - break - } - if (allMatch) { - foundSig = otherSig - break - } - } + const foundSig = this._findSubtypeImpl(imps, neededSig) if (foundSig) { corrected_self_references.push(foundSig) } else { @@ -963,18 +951,14 @@ 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 = {} - const template = this._templateTypes[base].spec + const newTypeSpec = {refines: base} + const maybeFrom = {} + const template = this.Templates[base].spec if (!template) { throw new Error( `Implementor error in _maybeAddTemplateType ${base} ${instantiator}`) } const instantiatorSpec = this.Types[instantiator] - if (instantiatorSpec.refines) { - // Assuming all templates are covariant, for now - this._maybeAddTemplateType(base, instantiatorSpec.refines) - newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` - } let beforeTypes = [] if (instantiatorSpec.before) { beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) @@ -988,9 +972,8 @@ export default class PocomathInstance { if (beforeTypes.length > 0) { newTypeSpec.before = beforeTypes } - newTypeSpec.test = template.test(instantiatorSpec.test) + newTypeSpec.test = template.test(this._typeTests[instantiator]) if (template.from) { - newTypeSpec.from = {} for (let source in template.from) { const instSource = substituteInSig( source, theTemplateParam, instantiator) @@ -1005,16 +988,87 @@ export default class PocomathInstance { for (const iFrom in instantiatorSpec.from) { const finalSource = substituteInSig( instSource, templateFromParam, iFrom) - newTypeSpec.from[finalSource] = template.from[source]( + maybeFrom[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } + // Assuming all templates are covariant here, I guess... + for (const subType of this._subtypes[instantiator]) { + const finalSource = substituteInSig( + instSource, templateFromParam, subType) + maybeFrom[finalSource] = template.from[source](x => x) + } } else { - newTypeSpec.from[instSource] = template.from[source] + maybeFrom[instSource] = template.from[source] } } } + + if (Object.keys(maybeFrom).length > 0) { + newTypeSpec.from = maybeFrom + } this.installType(wantsType, newTypeSpec) return wantsType } + _findSubtypeImpl(imps, neededSig) { + if (neededSig in imps) return neededSig + let foundSig = false + const typeList = typeListOfSignature(neededSig) + for (const otherSig in imps) { + const otherTypeList = typeListOfSignature(otherSig) + if (typeList.length !== otherTypeList.length) continue + let allMatch = true + for (let k = 0; k < typeList.length; ++k) { + let myType = typeList[k] + let otherType = otherTypeList[k] + if (otherType === theTemplateParam) { + otherTypeList[k] = 'ground' + otherType = 'ground' + } + if (otherType === '...T') { + otherTypeList[k] = '...ground' + otherType = 'ground' + } + const adjustedOtherType = otherType.replaceAll( + `<${theTemplateParam}>`, '') + if (adjustedOtherType !== otherType) { + otherTypeList[k] = adjustedOtherType + otherType = adjustedOtherType + } + if (myType.slice(0,3) === '...') myType = myType.slice(3) + if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) + if (otherType === 'any') continue + if (otherType === 'ground') continue + if (!(otherType in this.Types)) { + allMatch = false + break + } + if (myType === otherType + || this._subtypes[otherType].has(myType)) { + continue + } + allMatch = false + break + } + if (allMatch) { + foundSig = otherTypeList.join(',') + break + } + } + return foundSig + } + + _pocoresolve(name, sig) { + const typedfunc = this[name] + let result = undefined + try { + result = this._typed.find(typedfunc, sig, {exact: true}) + } catch { + } + if (result) return result + const foundsig = this._findSubtypeImpl(this._imps[name], sig) + if (foundsig) return this._typed.find(typedfunc, foundsig) + return this._typed.find(typedfunc, sig) + } + } diff --git a/src/core/utils.mjs b/src/core/utils.mjs index 11a879f..db164dd 100644 --- a/src/core/utils.mjs +++ b/src/core/utils.mjs @@ -6,6 +6,11 @@ export function subsetOfKeys(set, obj) { return true } +/* Returns a list of the types mentioned in a typed-function signature */ +export function typeListOfSignature(signature) { + return signature.split(',').map(s => s.trim()) +} + /* Returns a set of all of the types mentioned in a typed-function signature */ export function typesOfSignature(signature) { return new Set(signature.split(/[^\w\d]/).filter(s => s.length)) diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs index 04948df..00faddb 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -4,6 +4,7 @@ export * from './Types/generic.mjs' export const add = reducingOperation export const gcd = reducingOperation +export {identity} from './identity.mjs' export {lcm} from './lcm.mjs' export {mean} from './mean.mjs' export {mod} from './mod.mjs' diff --git a/src/generic/identity.mjs b/src/generic/identity.mjs new file mode 100644 index 0000000..2422d2f --- /dev/null +++ b/src/generic/identity.mjs @@ -0,0 +1,3 @@ +export function identity(x) { + return x +} diff --git a/src/generic/relational.mjs b/src/generic/relational.mjs index 368f394..939ae19 100644 --- a/src/generic/relational.mjs +++ b/src/generic/relational.mjs @@ -7,14 +7,27 @@ export const isZero = { } export const equal = { - '!T,T': ({ + 'any,any': ({equalTT, joinTypes, Templates, typeOf}) => (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)) + }) => (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 = { - 'T,T': ({'equal(T.T)': eq}) => (x,y) => !(eq(x,y)) + 'any,any': ({equal}) => (x,y) => !(equal(x,y)) } export const larger = { diff --git a/src/number/native.mjs b/src/number/native.mjs index 1404ab4..d095574 100644 --- a/src/number/native.mjs +++ b/src/number/native.mjs @@ -1,10 +1,12 @@ import gcdType from '../generic/gcdType.mjs' +import {identity} from '../generic/identity.mjs' export * from './Types/number.mjs' export {abs} from './abs.mjs' export {add} from './add.mjs' export {compare} from './compare.mjs' +export const conjugate = {number: () => identity} export const gcd = gcdType('NumInt') export {invert} from './invert.mjs' export {isZero} from './isZero.mjs' diff --git a/src/ops/floor.mjs b/src/ops/floor.mjs index 98d524a..e8897e8 100644 --- a/src/ops/floor.mjs +++ b/src/ops/floor.mjs @@ -12,7 +12,7 @@ export const floor = { // entry with type `bigint|NumInt|GaussianInteger` because they couldn't // be separately activated then - number: ({'equal(number,number)': eq}) => n => { + number: ({'equalTT(number,number)': eq}) => n => { if (eq(n, Math.round(n))) return Math.round(n) return Math.floor(n) }, diff --git a/src/pocomath.mjs b/src/pocomath.mjs index 53e646f..d8df045 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -4,18 +4,10 @@ import * as numbers from './number/native.mjs' import * as bigints from './bigint/native.mjs' import * as complex from './complex/native.mjs' import * as tuple from './tuple/native.mjs' -// Most of tuple is not ready yet: -const tupleReady = { - Tuple: tuple.Tuple, - equal: tuple.equal, - isZero: tuple.isZero, - length: tuple.length, - tuple: tuple.tuple -} import * as generic from './generic/all.mjs' import * as ops from './ops/all.mjs' const math = PocomathInstance.merge( - 'math', numbers, bigints, complex, tupleReady, generic, ops) + 'math', numbers, bigints, complex, tuple, generic, ops) export default math diff --git a/src/tuple/Types/Tuple.mjs b/src/tuple/Types/Tuple.mjs index 7b4ed35..0c6c0ae 100644 --- a/src/tuple/Types/Tuple.mjs +++ b/src/tuple/Types/Tuple.mjs @@ -23,7 +23,7 @@ Tuple.installType('Tuple', { from: { 'Tuple': convert => tu => ({elts: tu.elts.map(convert)}), // Here since there is no U it's a straight conversion: - T: t => ({elts: [u]}), // singleton promotion + T: t => ({elts: [t]}), // singleton promotion // Whereas the following will let you go directly from an element // convertible to T to a singleton Tuple. Not sure if we really // want that, but we'll try it just for kicks. @@ -32,48 +32,48 @@ Tuple.installType('Tuple', { }) Tuple.promoteUnary = { - 'Tuple': ({'self(T)': me}) => t => ({elts: t.elts.map(me)}) + 'Tuple': ({'self(T)': me, tuple}) => t => tuple(...(t.elts.map(me))) } Tuple.promoteBinaryUnary = { - 'Tuple,Tuple': ({'self(T,T)': meB, 'self(T)': meU}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) - else results.append(meU(s.elts[i])) + 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.append(meU(t.elts[i])) + if (i < t.elts.length) result.push(meU(t.elts[i])) else break } - return {elts: result} + return tuple(...result) } } Tuple.promoteBinary = { - 'Tuple,Tuple': ({'self(T,T)': meB}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) + result.push(meB(s.elts[i], t.elts[i])) } - return {elts: result} + return tuple(...result) } } Tuple.promoteBinaryStrict = { - 'Tuple,Tuple': ({'self(T,T)': meB}) => (s,t) => { + '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.append(meB(s.elts[i], t.elts[i])) + result.push(meB(s.elts[i], t.elts[i])) } - return {elts: result} + return tuple(...result) } } diff --git a/src/tuple/equal.mjs b/src/tuple/equal.mjs deleted file mode 100644 index 8186624..0000000 --- a/src/tuple/equal.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export * from './Types/Tuple.mjs' - -export const equal = { - // Change this to a template implementation (or maybe add template - // implementation to handle matching types, and have mixed template-base - // method returning false to catch test of two tuples of different types. - 'Tuple,Tuple': ({self, length}) => (s,t) => { - if (length(s) !== length(t)) return false - for (let i = 0; i < length(s); ++i) { - if (!self(s.elts[i], t.elts[i])) return false - } - return true - } -} diff --git a/src/tuple/equalTT.mjs b/src/tuple/equalTT.mjs new file mode 100644 index 0000000..1606410 --- /dev/null +++ b/src/tuple/equalTT.mjs @@ -0,0 +1,11 @@ +export * from './Types/Tuple.mjs' + +export const equalTT = { + 'Tuple,Tuple': ({'self(T,T)': me, 'length(Tuple)': len}) => (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/native.mjs b/src/tuple/native.mjs index fcf6f0d..66ba8f0 100644 --- a/src/tuple/native.mjs +++ b/src/tuple/native.mjs @@ -3,8 +3,8 @@ import {Tuple} from './Types/Tuple.mjs' export const add = Tuple.promoteBinaryUnary export const complex = Tuple.promoteBinaryStrict export const conjugate = Tuple.promoteUnary -// May want to replace equal with compare based on lexicographic ordering? -export {equal} from './equal.mjs' +export const divide = Tuple.promoteBinaryStrict +export {equalTT} from './equalTT.mjs' export const invert = Tuple.promoteUnary export {isZero} from './isZero.mjs' export {length} from './length.mjs' @@ -14,6 +14,7 @@ export const one = Tuple.promoteUnary export const quotient = Tuple.promoteBinaryStrict export const roundquotient = Tuple.promoteBinaryStrict export const sqrt = Tuple.promoteUnary +export const subtract = Tuple.promoteBinaryStrict export {tuple} from './tuple.mjs' export const zero = Tuple.promoteUnary diff --git a/test/custom.mjs b/test/custom.mjs index d232dd5..6a399b0 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -18,9 +18,10 @@ describe('A custom instance', () => { it("works when partially assembled", () => { bw.install(complex) // Not much we can call without any number types: - const i3 = {re: 0, im: 3} - assert.deepStrictEqual(bw.complex(0, 3), i3) - assert.deepStrictEqual(bw.chain(0).complex(3).value, i3) + assert.deepStrictEqual(bw.complex(undefined, undefined), undefined) + assert.deepStrictEqual( + bw.chain(undefined).complex(undefined).value, + undefined) // Don't have a way to negate things, for example: assert.throws(() => bw.negate(2), TypeError) }) @@ -34,7 +35,7 @@ describe('A custom instance', () => { assert.deepStrictEqual( bw.subtract(16, bw.add(3, bw.complex(0,4), 2)), math.complex(11, -4)) // note both instances coexist - assert.deepStrictEqual(bw.negate(math.complex(3, '8')).im, -8) + assert.deepStrictEqual(bw.negate(bw.complex(3, '8')).im, -8) }) it("can be assembled piecemeal", () => { @@ -130,12 +131,9 @@ describe('A custom instance', () => { inst.typeMerge(3, inst.complex(4.5,2.1)), 'Merge to Complex') // The following is the current behavior, since 3 converts to 3+0i - // and 3n converts to 3n+0ni, both of which are technically Complex. - // This will remain the case even with templated Complex, because - // both Complex and Complex will refine Complex (for the - // sake of catching new specializations). Not sure whether that will be - // OK or a problem that will have to be dealt with. - assert.strictEqual(inst.typeMerge(3, 3n), 'Merge to Complex') + // 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... diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs index 7727ac9..2cf56d1 100644 --- a/test/tuple/_native.mjs +++ b/test/tuple/_native.mjs @@ -18,11 +18,97 @@ describe('tuple', () => { {elts: [math.complex(3), math.complex(2n), math.complex(5.2)]}) }) - it('can be tested for zero', () => { + it('can be tested for zero and equality', () => { assert.strictEqual(math.isZero(math.tuple(0,1)), false) assert.strictEqual(math.isZero(math.tuple(0n,0n,0n,0n)), true) assert.strictEqual(math.isZero(math.tuple(0,0.001,0)), false) + assert.deepStrictEqual(math.complex(0,0), {re: 0, im:0}) assert.strictEqual(math.isZero(math.tuple(0,math.complex(0,0))), true) + assert.strictEqual( + math.equal( + math.tuple(0,math.complex(0,0.1)), + math.complex(math.tuple(0,0), math.tuple(0,0.1))), + true) + assert.strictEqual( + math.equal(math.tuple(3n,2n), math.tuple(3,2)), + false) + }) + + it('supports addition', () => { + assert.deepStrictEqual( + math.add(math.tuple(3,4,5), math.tuple(2,1,0)), + math.tuple(5,5,5)) + assert.deepStrictEqual( + math.add(math.tuple(3.25,4.5,5), math.tuple(3,3)), + math.tuple(6.25,7.5,5)) + assert.deepStrictEqual( + math.add(math.tuple(math.complex(2,3), 7), math.tuple(4, 5, 6)), + math.tuple(math.complex(6,3), math.complex(12), math.complex(6))) + assert.deepStrictEqual( + math.add(math.tuple(5,6), 7), + math.tuple(12,6)) + assert.deepStrictEqual( + math.add(math.tuple(math.complex(5,4),6), 7), + math.tuple(math.complex(12,4),math.complex(6))) + }) + + it('supports subtraction', () => { + assert.deepStrictEqual( + math.subtract(math.tuple(3n,4n,5n), math.tuple(2n,1n,0n)), + math.tuple(1n,3n,5n)) + assert.throws( + () => math.subtract(math.tuple(5,6), math.tuple(7)), + /RangeError/) + }) + + it('makes a tuple of complex and conjugates it', () => { + const complexTuple = math.tuple( + math.complex(3,1), math.complex(4,2.2), math.complex(5,3)) + assert.deepStrictEqual( + math.complex(math.tuple(3,4,5), math.tuple(1,2.2,3)), + complexTuple) + assert.deepStrictEqual( + math.conjugate(complexTuple), + math.tuple(math.complex(3,-1), math.complex(4,-2.2), math.complex(5,-3))) + }) + + it('supports division', () => { + assert.deepStrictEqual( + math.divide(math.tuple(3,4,5),math.tuple(1,2,2)), + math.tuple(3,2,2.5)) + }) + + it('supports multiplication', () => { + assert.deepStrictEqual( + math.multiply(math.tuple(3,4,5), math.tuple(1,2,2)), + math.tuple(3,8,10)) + }) + + it('supports one and zero', () => { + assert.deepStrictEqual( + math.one(math.tuple(2n,3n,0n)), + math.tuple(1n,1n,1n)) + assert.deepStrictEqual( + math.zero(math.tuple(math.complex(5,2), 3.4)), + math.tuple(math.complex(0), math.complex(0))) + }) + + it('supports quotient and roundquotient', () => { + const bigTuple = math.tuple(1n,2n,3n,4n,5n) + const bigOnes = math.one(bigTuple) + const threes = math.add(bigOnes, bigOnes, bigOnes) + assert.deepStrictEqual( + math.quotient(bigTuple, threes), + math.tuple(0n, 0n, 1n, 1n, 1n)) + assert.deepStrictEqual( + math.roundquotient(bigTuple, threes), + math.tuple(0n, 1n, 1n, 1n, 2n)) + }) + + it('supports sqrt', () => { + assert.deepStrictEqual( + math.sqrt(math.tuple(4,-4,2.25)), + math.tuple(2, math.complex(0,2), 1.5)) }) }) -- 2.34.1