From 845a2354c988750df76f699f34d8ed75104b8c2b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 5 Aug 2022 12:48:57 +0000 Subject: [PATCH] feat: Template types (#45) Includes a full implementation of a type-homogeneous Tuple type, using the template types feature, as a demonstration/check of its operation. Co-authored-by: Glen Whitney Reviewed-on: https://code.studioinfinity.org/glen/pocomath/pulls/45 --- src/bigint/native.mjs | 2 + src/complex/add.mjs | 8 +- src/complex/associate.mjs | 17 + src/complex/complex.mjs | 3 +- src/complex/{equal.mjs => equalTT.mjs} | 2 +- src/complex/gcd.mjs | 15 +- src/complex/native.mjs | 7 +- src/core/PocomathInstance.mjs | 613 +++++++++++++++--- src/core/dependencyExtractor.mjs | 12 - src/core/extractors.mjs | 41 ++ src/core/utils.mjs | 5 + src/generic/arithmetic.mjs | 2 + src/generic/identity.mjs | 3 + src/generic/lcm.mjs | 3 + src/generic/relational.mjs | 19 +- src/number/native.mjs | 2 + src/ops/floor.mjs | 2 +- src/pocomath.mjs | 3 +- src/tuple/Types/Tuple.mjs | 80 +++ src/tuple/equalTT.mjs | 11 + src/tuple/isZero.mjs | 8 + src/tuple/length.mjs | 3 + src/tuple/native.mjs | 21 + src/tuple/tuple.mjs | 6 + test/_pocomath.mjs | 9 + ...yExtractor.mjs => dependencyExtractor.mjs} | 2 +- test/custom.mjs | 36 +- test/tuple/_native.mjs | 114 ++++ 28 files changed, 920 insertions(+), 129 deletions(-) create mode 100644 src/complex/associate.mjs rename src/complex/{equal.mjs => equalTT.mjs} (95%) delete mode 100644 src/core/dependencyExtractor.mjs create mode 100644 src/core/extractors.mjs create mode 100644 src/generic/identity.mjs create mode 100644 src/tuple/Types/Tuple.mjs create mode 100644 src/tuple/equalTT.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 rename test/core/{_dependencyExtractor.mjs => dependencyExtractor.mjs} (89%) create mode 100644 test/tuple/_native.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 new file mode 100644 index 0000000..f3106b4 --- /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, + 'equalTT(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/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/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..420ce88 100644 --- a/src/complex/native.mjs +++ b/src/complex/native.mjs @@ -1,13 +1,12 @@ -import gcdType from '../generic/gcdType.mjs' - export * from './Types/Complex.mjs' export {abs} from './abs.mjs' export {absquare} from './absquare.mjs' export {add} from './add.mjs' -export {conjugate} from './conjugate.mjs' +export {associate} from './associate.mjs' export {complex} from './complex.mjs' -export {equal} from './equal.mjs' +export {conjugate} from './conjugate.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 6fab6c9..05636d4 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,12 +1,13 @@ /* 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 {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 templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type @@ -27,7 +28,10 @@ export default class PocomathInstance { 'importDependencies', 'install', 'installType', + 'joinTypes', 'name', + 'self', + 'Templates', 'typeOf', 'Types', 'undefinedTypes' @@ -39,11 +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.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. @@ -52,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, { @@ -68,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) } /** @@ -177,9 +193,12 @@ 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.Templates)) { + this._installTemplateType(info.type, info.spec) + } const migrateImps = {} for (const operator in other._imps) { if (operator != 'typeOf') { // skip the builtin, we already have it @@ -262,10 +281,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}`) @@ -278,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 @@ -291,68 +315,129 @@ 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 + let nextSuper = spec.refines + while (nextSuper) { + 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) { // 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) 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} 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 === '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 + /* 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: */ 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 */ + _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.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.Templates[base] = {type, spec} } /* Used internally by install, see the documentation there */ @@ -392,9 +477,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._seenTypes.add(word) + this._addAffect(':' + word, name) + } } } } @@ -420,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 + } }) } @@ -457,22 +545,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.Templates) 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)}`) } + /* 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) { @@ -485,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) } } @@ -502,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() @@ -521,44 +619,170 @@ 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}) + 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 = rawSignature.replaceAll(templateCall, '') + /* Any remaining template params are top-level */ const signature = substituteInSig( - trimSignature, theTemplateParam, 'any') + baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call - * and add the new instantiations + * and add the new instantiations. + * First, prepare 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 - } - } - if (exemplar < 0) { + const parTypes = rawSignature.split(',') + const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') + const topTyper = entity => this.typeOf(entity) + const inferences = parTypes.map( + type => generateTypeExtractor( + type, + theTemplateParam, + topTyper, + this.joinTypes.bind(this), + this.Templates)) + if (inferences.every(x => !x)) { // all false 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) => { - const example = args[exemplar] - const instantiateFor = self.typeOf(example) + /* 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(', ')) + } + 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( + `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 */ + const wantTypes = parTypes.map(type => substituteInSig( + type, theTemplateParam, instantiateFor)) + /* Now we have to add any actual types that are relevant + * to this invocation. Namely, that would be every formal parameter + * type in the invocation, with the parameter template instantiated + * by instantiateFor, and for all of instantiateFor's "prior types" + */ + for (j = 0; j < parTypes.length; ++j) { + if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { + // actually used the param and is a template + self._ensureTemplateTypes(parTypes[j], instantiateFor) + } + } + /* Transform the arguments if we used any conversions: */ + if (usedConversions) { + i = - 1 + for (j = 0; j < args.length; ++j) { + if (i < parTypes.length - 1) ++i + let wantType = parTypes[i] + if (wantType.slice(0,3) === '...') { + wantType = wantType.slice(3) + } + wantType = substituteInSig( + wantType, theTemplateParam, instantiateFor) + if (wantType !== parTypes[i]) { + args[j] = self._typed.convert(args[j], wantType) + } + } + } + /* Finally reassemble the rest args if there were any */ + if (restParam) { + const restArgs = args.slice(regLength) + args = args.slice(0,regLength) + args.push(restArgs) + } + /* Arrange that the desired instantiation will be there next + * time so we don't have to go through that again for this type + */ refs[theTemplateParam] = instantiateFor behavior.instantiations.add(instantiateFor) self._invalidate(name) - // And for now, we have to rely on the "any" implementation. Hope - // it matches the instantiated one! - return behavior.does(refs)(...args) + // And update refs because we now know the type we're instantiating + // for: + const innerRefs = {} + for (const dep in simplifiedUses) { + const simplifiedDep = simplifiedUses[dep] + if (dep === simplifiedDep) { + innerRefs[dep] = refs[dep] + } else { + let [func, needsig] = dep.split(/[()]/) + if (self._typed.isTypedFunction(refs[simplifiedDep])) { + const subsig = substituteInSig( + needsig, theTemplateParam, instantiateFor) + let resname = simplifiedDep + if (resname === 'self') resname = name + innerRefs[dep] = self._pocoresolve(resname, subsig) + } else { + innerRefs[dep] = refs[simplifiedDep] + } + } + } + // Finally ready to make the call. + 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 @@ -579,9 +803,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) { @@ -614,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 } @@ -628,16 +860,215 @@ 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, 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(imps, neededSig) + 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 + * instantiations of it for type and all prior types of type are present + * 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.add(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 = {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] + 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(this._typeTests[instantiator]) + if (template.from) { + for (let source in template.from) { + const instSource = substituteInSig( + source, theTemplateParam, instantiator) + let usesFromParam = false + for (const word of instSource.split(/[<>]/)) { + if (word === templateFromParam) { + usesFromParam = true + break + } + } + if (usesFromParam) { + for (const iFrom in instantiatorSpec.from) { + const finalSource = substituteInSig( + instSource, templateFromParam, iFrom) + maybeFrom[finalSource] = template.from[source]( + instantiatorSpec.from[iFrom]) + } + // Assuming all templates are covariant here, I guess... + for (const subType of this._subtypes[instantiator]) { + const finalSource = substituteInSig( + instSource, templateFromParam, subType) + maybeFrom[finalSource] = template.from[source](x => x) + } + } else { + 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/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..0db3c0f --- /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, typeJoiner, templates) + if (!argExtractor) return false + return templates[base].spec.infer({ + typeOf: argExtractor, + joinTypes: typeJoiner + }) +} 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 f6abd23..00faddb 100644 --- a/src/generic/arithmetic.mjs +++ b/src/generic/arithmetic.mjs @@ -3,6 +3,8 @@ import {reducingOperation} from './reducingOperation.mjs' 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/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/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 dee980e..d8df045 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -3,10 +3,11 @@ 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' 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, tuple, 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..0c6c0ae --- /dev/null +++ b/src/tuple/Types/Tuple.mjs @@ -0,0 +1,80 @@ +/* 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', { + // 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 + 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: [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. + U: convert => u => ({elts: [convert(u)]}) + } +}) + +Tuple.promoteUnary = { + 'Tuple': ({'self(T)': me, tuple}) => t => tuple(...(t.elts.map(me))) +} + +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 + } + if (i < t.elts.length) result.push(meU(t.elts[i])) + else break + } + 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.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) + } +} + +export {Tuple} 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/isZero.mjs b/src/tuple/isZero.mjs new file mode 100644 index 0000000..9375277 --- /dev/null +++ b/src/tuple/isZero.mjs @@ -0,0 +1,8 @@ +export {Tuple} from './Types/Tuple.mjs' + +export const 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/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..66ba8f0 --- /dev/null +++ b/src/tuple/native.mjs @@ -0,0 +1,21 @@ +import {Tuple} from './Types/Tuple.mjs' + +export const add = Tuple.promoteBinaryUnary +export const complex = Tuple.promoteBinaryStrict +export const conjugate = Tuple.promoteUnary +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' +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 const subtract = Tuple.promoteBinaryStrict +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..893b54d --- /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 = {'...T': () => args => ({elts: args})} 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))) + }) + }) 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', () => { diff --git a/test/custom.mjs b/test/custom.mjs index 9fb66b1..6a399b0 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' @@ -17,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) }) @@ -33,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", () => { @@ -112,4 +114,30 @@ 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 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... + inst.install(genericSubtract) + assert.throws(() => inst.typeMerge(3, undefined), TypeError) + }) }) diff --git a/test/tuple/_native.mjs b/test/tuple/_native.mjs new file mode 100644 index 0000000..2cf56d1 --- /dev/null +++ b/test/tuple/_native.mjs @@ -0,0 +1,114 @@ +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, 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 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)) + }) + +})