From 91ec20edd856c999e5428a2844e62f95e7282eb8 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 25 Jul 2022 04:20:13 -0700 Subject: [PATCH] feat: Implement signature-specifc reference Also implements a config object that upon change, lazily invalidates all operations that access it. Also allows references to signatures with nonexistent types (which typed-function does not); they come back as undefined. Uses these features to implement sqrt for number and complex. Resolves #7. --- src/complex/abs.mjs | 5 ++ src/complex/all.mjs | 7 +-- src/complex/native.mjs | 8 +++ src/complex/sqrt.mjs | 37 ++++++++++++ src/core/PocomathInstance.mjs | 99 +++++++++++++++++++++++++++----- src/core/dependencyExtractor.mjs | 5 +- src/generic/all.mjs | 1 + src/generic/arithmetic.mjs | 3 + src/generic/divide.mjs | 4 ++ src/generic/sign.mjs | 6 ++ src/number/abs.mjs | 3 + src/number/all.mjs | 6 +- src/number/invert.mjs | 3 + src/number/multiply.mjs | 5 ++ src/number/native.js | 4 ++ src/number/native.mjs | 8 +++ src/number/sqrt.mjs | 14 +++++ src/pocomath.mjs | 6 +- test/complex/## | 0 test/complex/_all.mjs | 29 ++++++++++ test/number/_all.mjs | 29 ++++++++++ 21 files changed, 256 insertions(+), 26 deletions(-) create mode 100644 src/complex/abs.mjs create mode 100644 src/complex/native.mjs create mode 100644 src/complex/sqrt.mjs create mode 100644 src/generic/all.mjs create mode 100644 src/generic/arithmetic.mjs create mode 100644 src/generic/divide.mjs create mode 100644 src/generic/sign.mjs create mode 100644 src/number/abs.mjs create mode 100644 src/number/invert.mjs create mode 100644 src/number/multiply.mjs create mode 100644 src/number/native.js create mode 100644 src/number/native.mjs create mode 100644 src/number/sqrt.mjs create mode 100755 test/complex/## create mode 100644 test/complex/_all.mjs create mode 100644 test/number/_all.mjs diff --git a/src/complex/abs.mjs b/src/complex/abs.mjs new file mode 100644 index 0000000..20ab809 --- /dev/null +++ b/src/complex/abs.mjs @@ -0,0 +1,5 @@ +export {Types} from './Types/Complex.mjs' + +export const abs = {Complex: ({sqrt, add, multiply}) => z => { + return sqrt(add(multiply(z.re, z.re), multiply(z.im, z.im))) +}} diff --git a/src/complex/all.mjs b/src/complex/all.mjs index d5d5bda..2c3946d 100644 --- a/src/complex/all.mjs +++ b/src/complex/all.mjs @@ -1,5 +1,2 @@ -export {Types} from './Types/Complex.mjs' -export {complex} from './complex.mjs' -export {add} from './add.mjs' -export {negate} from './negate.mjs' -export {subtract} from '../generic/subtract.mjs' +export * from './native.mjs' +export * from '../generic/arithmetic.mjs' diff --git a/src/complex/native.mjs b/src/complex/native.mjs new file mode 100644 index 0000000..aec0868 --- /dev/null +++ b/src/complex/native.mjs @@ -0,0 +1,8 @@ +export {Types} from './Types/Complex.mjs' + +export {abs} from './abs.mjs' +export {add} from './add.mjs' +export {complex} from './complex.mjs' +export {negate} from './negate.mjs' +export {sqrt} from './sqrt.mjs' + diff --git a/src/complex/sqrt.mjs b/src/complex/sqrt.mjs new file mode 100644 index 0000000..93cd10a --- /dev/null +++ b/src/complex/sqrt.mjs @@ -0,0 +1,37 @@ +export { Types } from './Types/Complex.mjs' + +export const sqrt = { + Complex: ({ + config, + complex, + multiply, + sign, + self, + divide, + add, + 'abs(Complex)': abs, + subtract + }) => { + if (config.predictable) { + return z => { + const imSign = sign(z.im) + const reSign = sign(z.re) + if (imSign === 0 && reSign === 1) return complex(self(z.re)) + return complex( + multiply(sign(z.im), self(divide(add(abs(z),z.re), 2))), + self(divide(subtract(abs(z),z.re), 2)) + ) + } + } + return z => { + const imSign = sign(z.im) + const reSign = sign(z.re) + if (imSign === 0 && reSign === 1) return self(z.re) + return complex( + multiply(sign(z.im), self(divide(add(abs(z),z.re), 2))), + self(divide(subtract(abs(z),z.re), 2)) + ) + } + } +} + diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 8e66280..646bd62 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -8,7 +8,7 @@ export default class PocomathInstance { * in that if a new top-level PocomathInstance method is added, its name * must be added to this list. */ - static reserved = new Set(['install', 'importDependencies']) + static reserved = new Set(['config', 'importDependencies', 'install', 'name']) constructor(name) { this.name = name @@ -17,6 +17,19 @@ export default class PocomathInstance { this._typed = typed.create() this._typed.clear() this.Types = {any: {}} // dummy entry to track the default 'any' type + this._doomed = new Set() // for detecting circular reference + this._config = {predictable: false} + const self = this + this.config = new Proxy(this._config, { + get: (target, property) => target[property], + set: (target, property, value) => { + if (value !== target[property]) { + target[property] = value + self._invalidateDependents('config') + } + return true // successful + } + }) } /** @@ -80,6 +93,8 @@ export default class PocomathInstance { * @param {string[]} types A list of type names */ async importDependencies(types) { + const typeSet = new Set(types) + typeSet.add('generic') const doneSet = new Set(['self']) // nothing to do for self dependencies while (true) { const requiredSet = new Set() @@ -96,7 +111,7 @@ export default class PocomathInstance { } if (requiredSet.size === 0) break for (const name of requiredSet) { - for (const type of types) { + for (const type of typeSet) { try { const modName = `../${type}/${name}.mjs` const mod = await import(modName) @@ -145,7 +160,8 @@ export default class PocomathInstance { } } this.Types[type] = spec - this._invalidate(':' + type) // rebundle anything that uses the new type + // rebundle anything that uses the new type: + this._invalidateDependents(':' + type) } } @@ -198,14 +214,27 @@ 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 (!(name in this._imps)) { + this._imps[name] = {} + } + this._doomed.add(name) + this._invalidateDependents(name) + this._doomed.delete(name) const self = this Object.defineProperty(this, name, { configurable: true, get: () => self._bundle(name) }) - if (!(name in this._imps)) { - this._imps[name] = {} - } + } + + /** + * Invalidate all the dependents of a given property of the instance + */ + _invalidateDependents(name) { if (name in this._affects) { for (const ancestor of this._affects[name]) { this._invalidate(ancestor) @@ -229,29 +258,69 @@ export default class PocomathInstance { `Every implementation for ${name} uses an undefined type;\n` + ` signatures: ${Object.keys(imps)}`) } + Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) const tf_imps = {} for (const [signature, {uses, does}] of usableEntries) { if (uses.length === 0) { tf_imps[signature] = does() } else { const refs = {} - let self_referential = false + let full_self_referential = false + let part_self_references = [] for (const dep of uses) { - // TODO: handle signature-specific dependencies - if (dep.includes('(')) { - throw new Error('signature specific reference unimplemented') - } - if (dep === 'self') { - self_referential = true + const [func, needsig] = dep.split(/[()]/) + if (func === 'self') { + if (needsig) { + if (full_self_referential) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + if (subsetOfKeys(typesOfSignature(needsig), this.Types)) { + part_self_references.push(needsig) + } + } else { + if (part_self_references.length) { + throw new SyntaxError( + 'typed-function does not support mixed full and ' + + 'partial self-reference') + } + full_self_referential = true + } } else { - refs[dep] = this[dep] // assume acyclic for now + if (this[func] === 'limbo') { + /* We are in the midst of bundling func, so have to use + * an indirect reference to func. And given that, there's + * really no helpful way to extract a specific signature + */ + const self = this + refs[dep] = function () { // is this the most efficient? + return self[func].apply(this, arguments) + } + } else { + // can bundle up func, and grab its signature if need be + let destination = this[func] + if (needsig) { + destination = this._typed.find(destination, needsig) + } + refs[dep] = destination + } } } - if (self_referential) { + if (full_self_referential) { tf_imps[signature] = this._typed.referToSelf(self => { refs.self = self return does(refs) }) + } else if (part_self_references.length) { + tf_imps[signature] = this._typed.referTo( + ...part_self_references, (...impls) => { + for (let i = 0; i < part_self_references.length; ++i) { + refs[`self(${part_self_references[i]})`] = impls[i] + } + return does(refs) + } + ) } else { tf_imps[signature] = does(refs) } diff --git a/src/core/dependencyExtractor.mjs b/src/core/dependencyExtractor.mjs index fbf5611..1b1091c 100644 --- a/src/core/dependencyExtractor.mjs +++ b/src/core/dependencyExtractor.mjs @@ -4,6 +4,9 @@ */ export default function dependencyExtractor(destinationSet) { return new Proxy({}, { - get: (target, property) => { destinationSet.add(property) } + get: (target, property) => { + destinationSet.add(property) + return {} + } }) } diff --git a/src/generic/all.mjs b/src/generic/all.mjs new file mode 100644 index 0000000..db678dd --- /dev/null +++ b/src/generic/all.mjs @@ -0,0 +1 @@ +export * from './arithmetic.mjs' diff --git a/src/generic/arithmetic.mjs b/src/generic/arithmetic.mjs new file mode 100644 index 0000000..2787c41 --- /dev/null +++ b/src/generic/arithmetic.mjs @@ -0,0 +1,3 @@ +export {divide} from './divide.mjs' +export {sign} from './sign.mjs' +export {subtract} from './subtract.mjs' diff --git a/src/generic/divide.mjs b/src/generic/divide.mjs new file mode 100644 index 0000000..4dcfc89 --- /dev/null +++ b/src/generic/divide.mjs @@ -0,0 +1,4 @@ +export const divide = { + 'any,any': ({multiply, invert}) => (x, y) => multiply(x, invert(y)) +} + diff --git a/src/generic/sign.mjs b/src/generic/sign.mjs new file mode 100644 index 0000000..c8133fd --- /dev/null +++ b/src/generic/sign.mjs @@ -0,0 +1,6 @@ +export const sign = { + any: ({negate, divide, abs}) => x => { + if (x === negate(x)) return x // zero + return divide(x, abs(x)) + } +} diff --git a/src/number/abs.mjs b/src/number/abs.mjs new file mode 100644 index 0000000..1613356 --- /dev/null +++ b/src/number/abs.mjs @@ -0,0 +1,3 @@ +export {Types} from './Types/number.mjs' + +export const abs = {number: () => n => Math.abs(n)} diff --git a/src/number/all.mjs b/src/number/all.mjs index bf737f5..e437adb 100644 --- a/src/number/all.mjs +++ b/src/number/all.mjs @@ -1,4 +1,4 @@ export {Types} from './Types/number.mjs' -export {add} from './add.mjs' -export {negate} from './negate.mjs' -export {subtract} from '../generic/subtract.mjs' + +export * from './native.mjs' +export * from '../generic/arithmetic.mjs' diff --git a/src/number/invert.mjs b/src/number/invert.mjs new file mode 100644 index 0000000..11d24c3 --- /dev/null +++ b/src/number/invert.mjs @@ -0,0 +1,3 @@ +export {Types} from './Types/number.mjs' + +export const invert = {number: () => n => 1/n} diff --git a/src/number/multiply.mjs b/src/number/multiply.mjs new file mode 100644 index 0000000..dff6611 --- /dev/null +++ b/src/number/multiply.mjs @@ -0,0 +1,5 @@ +export {Types} from './Types/number.mjs' + +export const multiply = { + '...number': () => multiplicands => multiplicands.reduce((x,y) => x*y, 1), +} diff --git a/src/number/native.js b/src/number/native.js new file mode 100644 index 0000000..208d8a2 --- /dev/null +++ b/src/number/native.js @@ -0,0 +1,4 @@ +export {Types} from './Types/number.mjs' +export {add} from './add.mjs' +export {negate} from './negate.mjs' +export {sqrt} from './sqrt.mjs' diff --git a/src/number/native.mjs b/src/number/native.mjs new file mode 100644 index 0000000..99807de --- /dev/null +++ b/src/number/native.mjs @@ -0,0 +1,8 @@ +export {Types} from './Types/number.mjs' + +export {abs} from './abs.mjs' +export {add} from './add.mjs' +export {invert} from './invert.mjs' +export {multiply} from './multiply.mjs' +export {negate} from './negate.mjs' +export {sqrt} from './sqrt.mjs' diff --git a/src/number/sqrt.mjs b/src/number/sqrt.mjs new file mode 100644 index 0000000..179aa0a --- /dev/null +++ b/src/number/sqrt.mjs @@ -0,0 +1,14 @@ +export { Types } from './Types/number.mjs' + +export const sqrt = { + number: ({config, complex, 'self(Complex)': complexSqrt}) => { + if (config.predictable || !complexSqrt) { + return n => isNaN(n) ? NaN : Math.sqrt(n) + } + return n => { + if (isNaN(n)) return NaN + if (n >= 0) return Math.sqrt(n) + return complexSqrt(complex(n)) + } + } +} diff --git a/src/pocomath.mjs b/src/pocomath.mjs index c5a81d4..e0015a9 100644 --- a/src/pocomath.mjs +++ b/src/pocomath.mjs @@ -1,12 +1,14 @@ /* Core of pocomath: generates the default instance */ import PocomathInstance from './core/PocomathInstance.mjs' -import * as numbers from './number/all.mjs' +import * as numbers from './number/native.mjs' import * as bigints from './bigint/all.mjs' -import * as complex from './complex/all.mjs' +import * as complex from './complex/native.mjs' +import * as generic from './generic/all.mjs' const math = new PocomathInstance('math') math.install(numbers) math.install(bigints) math.install(complex) +math.install(generic) export default math diff --git a/test/complex/## b/test/complex/## new file mode 100755 index 0000000..e69de29 diff --git a/test/complex/_all.mjs b/test/complex/_all.mjs new file mode 100644 index 0000000..9d09222 --- /dev/null +++ b/test/complex/_all.mjs @@ -0,0 +1,29 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' +import PocomathInstance from '../../src/core/PocomathInstance.mjs' +import * as complexSqrt from '../../src/complex/sqrt.mjs' + +describe('complex', () => { + it('supports sqrt', () => { + assert.deepStrictEqual(math.sqrt(math.complex(1,0)), 1) + assert.deepStrictEqual( + math.sqrt(math.complex(0,1)), + math.complex(math.sqrt(0.5), math.sqrt(0.5))) + math.config.predictable = true + assert.deepStrictEqual(math.sqrt(math.complex(1,0)), math.complex(1,0)) + assert.deepStrictEqual( + math.sqrt(math.complex(0,1)), + math.complex(math.sqrt(0.5), math.sqrt(0.5))) + math.config.predictable = false + }) + + it('can bundle sqrt', async function () { + const ms = new PocomathInstance('Minimal Sqrt') + ms.install(complexSqrt) + await ms.importDependencies(['number', 'complex']) + assert.deepStrictEqual( + ms.sqrt(math.complex(0, -1)), + math.complex(ms.negate(ms.sqrt(0.5)), ms.sqrt(0.5))) + }) + +}) diff --git a/test/number/_all.mjs b/test/number/_all.mjs new file mode 100644 index 0000000..b334fef --- /dev/null +++ b/test/number/_all.mjs @@ -0,0 +1,29 @@ +import assert from 'assert' +import math from '../../src/pocomath.mjs' +import PocomathInstance from '../../src/core/PocomathInstance.mjs' +import * as numberSqrt from '../../src/number/sqrt.mjs' +import * as complex from '../../src/complex/all.mjs' +import * as numbers from '../../src/number/all.mjs' +describe('number', () => { + it('supports sqrt', () => { + assert.strictEqual(math.sqrt(4), 2) + assert.strictEqual(math.sqrt(NaN), NaN) + assert.strictEqual(math.sqrt(2.25), 1.5) + assert.deepStrictEqual(math.sqrt(-9), math.complex(0, 3)) + math.config.predictable = true + assert.strictEqual(math.sqrt(-9), NaN) + math.config.predictable = false + assert.deepStrictEqual(math.sqrt(-0.25), math.complex(0, 0.5)) + }) + + it('supports sqrt by itself', () => { + const no = new PocomathInstance('Numbers Only') + no.install(numberSqrt) + assert.strictEqual(no.sqrt(2.56), 1.6) + assert.strictEqual(no.sqrt(-17), NaN) + no.install(complex) + no.install(numbers) + assert.deepStrictEqual(no.sqrt(-16), no.complex(0,4)) + }) + +})