From 358f68fbbd5c22e624144acb7865c9f267fea741 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 24 Jul 2022 12:52:05 -0700 Subject: [PATCH] feat(Types): Improve type spec and ignore signatures that use unknown type --- src/bigint/Types/bigint.mjs | 5 +- src/complex/Types/Complex.mjs | 6 +- src/core/PocomathInstance.mjs | 140 +++++++++++++++++++--------------- src/core/utils.mjs | 12 +++ src/number/Types/number.mjs | 3 +- test/_pocomath.mjs | 16 +++- test/custom.mjs | 17 +++++ 7 files changed, 129 insertions(+), 70 deletions(-) create mode 100644 src/core/utils.mjs diff --git a/src/bigint/Types/bigint.mjs b/src/bigint/Types/bigint.mjs index 45ece8f..04bd189 100644 --- a/src/bigint/Types/bigint.mjs +++ b/src/bigint/Types/bigint.mjs @@ -1,3 +1,6 @@ export const Types = { - bigint: {test: b => typeof b === 'bigint'} + bigint: { + before: ['Complex'], + test: b => typeof b === 'bigint' + } } diff --git a/src/complex/Types/Complex.mjs b/src/complex/Types/Complex.mjs index e3f29e1..f6e3eb2 100644 --- a/src/complex/Types/Complex.mjs +++ b/src/complex/Types/Complex.mjs @@ -9,8 +9,10 @@ export function isComplex(z) { export const Types = { Complex: { test: isComplex, - number: x => ({re: x, im: 0}), - bigint: x => ({re: x, im: 0n}) + from: { + number: x => ({re: x, im: 0}), + bigint: x => ({re: x, im: 0n}) + } } } diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 83a11f8..8e66280 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -1,6 +1,7 @@ /* Core of pocomath: create an instance */ import typed from 'typed-function' import dependencyExtractor from './dependencyExtractor.mjs' +import {subsetOfKeys, typesOfSignature} from './utils.mjs' export default class PocomathInstance { /* Disallowed names for ops; beware, this is slightly non-DRY @@ -15,10 +16,9 @@ export default class PocomathInstance { this._affects = {} this._typed = typed.create() this._typed.clear() - // Convenient hack for now, would remove when a real string type is added: - this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}]) + this.Types = {any: {}} // dummy entry to track the default 'any' type } - + /** * (Partially) define one or more operations of the instance: * @@ -54,13 +54,23 @@ export default class PocomathInstance { * * Note that the "operation" named `Types` is special: it gives * types that must be installed in the instance. In this case, the keys - * are type names, and the values are objects with a property 'test' - * giving the predicate for the type, and properties for each type that can - * be converted **to** this type, giving the corresponding conversion - * function. + * are type names, and the values are plain objects with the following + * properties: + * + * - test: the predicate for the type + * - from: a plain object mapping the names of types that can be converted + * **to** this type to the corresponding conversion functions + * - before: [optional] a list of types this should be added + * before, in priority order */ install(ops) { - for (const key in ops) this._installOp(key, ops[key]) + for (const [item, spec] of Object.entries(ops)) { + if (item === 'Types') { + this._installTypes(spec) + } else { + this._installOp(item, spec) + } + } } /** @@ -99,6 +109,45 @@ export default class PocomathInstance { } } } + + /* Used internally by install, see the documentation there. + * Note that unlike _installOp below, we can do this immediately + */ + _installTypes(typeSpecs) { + for (const [type, spec] of Object.entries(typeSpecs)) { + if (type in this.Types) { + if (spec !== this.Types[type]) { + throw new SyntaxError( + `Conflicting definitions of type ${type}`) + } + continue + } + let beforeType = 'any' + for (const other of spec.before || []) { + if (other in this.Types) { + beforeType = other + break + } + } + this._typed.addTypes([{name: type, test: spec.test}], beforeType) + /* Now add conversions to this type */ + for (const from in (spec.from || {})) { + if (from in this.Types) { + this._typed.addConversion( + {from, to: type, convert: spec.from[from]}) + } + } + /* And add conversions from this type */ + for (const to in this.Types) { + if (type in (this.Types[to].from || {})) { + this._typed.addConversion( + {from: type, to, convert: this.Types[to].from[type]}) + } + } + this.Types[type] = spec + this._invalidate(':' + type) // rebundle anything that uses the new type + } + } /* Used internally by install, see the documentation there */ _installOp(name, implementations) { @@ -115,42 +164,35 @@ export default class PocomathInstance { this._invalidate(name) const opImps = this._imps[name] for (const [signature, does] of Object.entries(implementations)) { - if (name === 'Types') { - if (signature in opImps) { - if (does != opImps[signature]) { - throw newSyntaxError( - `Conflicting definitions of type ${signature}`) - } - } else { - opImps[signature] = does - } - continue - } if (signature in opImps) { if (does !== opImps[signature].does) { throw new SyntaxError( `Conflicting definitions of ${signature} for ${name}`) } } else { - if (name === 'Types') { - opImps[signature] = does - continue - } const uses = new Set() does(dependencyExtractor(uses)) opImps[signature] = {uses, does} for (const dep of uses) { const depname = dep.split('(', 1)[0] if (depname === 'self') continue - if (!(depname in this._affects)) { - this._affects[depname] = new Set() - } - this._affects[depname].add(name) + this._addAffect(depname, name) + } + for (const type of typesOfSignature(signature)) { + this._addAffect(':' + type, name) } } } } + _addAffect(dependency, dependent) { + if (dependency in this._affects) { + this._affects[dependency].add(dependent) + } else { + this._affects[dependency] = new Set([dependent]) + } + } + /** * Reset an operation to require creation of typed-function, * and if it has no implementations so far, set them up. @@ -177,12 +219,18 @@ export default class PocomathInstance { */ _bundle(name) { const imps = this._imps[name] - if (!imps || Object.keys(imps).length === 0) { + if (!imps) { throw new SyntaxError(`No implementations for ${name}`) } - this._ensureTypes() + const usableEntries = Object.entries(imps).filter( + ([signature]) => subsetOfKeys(typesOfSignature(signature), this.Types)) + if (usableEntries.length === 0) { + throw new SyntaxError( + `Every implementation for ${name} uses an undefined type;\n` + + ` signatures: ${Object.keys(imps)}`) + } const tf_imps = {} - for (const [signature, {uses, does}] of Object.entries(imps)) { + for (const [signature, {uses, does}] of usableEntries) { if (uses.length === 0) { tf_imps[signature] = does() } else { @@ -214,36 +262,4 @@ export default class PocomathInstance { return tf } - /** - * Ensure that all of the requested types and conversions are actually - * in the typed-function universe: - */ - _ensureTypes() { - const newTypes = [] - const newTypeSet = new Set() - const knownTypeSet = new Set() - const conversions = [] - const typeSpec = this._imps.Types - for (const name in this._imps.Types) { - knownTypeSet.add(name) - for (const from in typeSpec[name]) { - if (from === 'test') continue; - conversions.push( - {from, to: name, convert: typeSpec[name][from]}) - } - try { // Hack: work around typed-function #154 - this._typed._findType(name) - } catch { - newTypeSet.add(name) - newTypes.push({name, test: typeSpec[name].test}) - } - } - this._typed.addTypes(newTypes) - const newConversions = conversions.filter( - item => (newTypeSet.has(item.from) || newTypeSet.has(item.to)) && - knownTypeSet.has(item.from) && knownTypeSet.has(item.to) - ) - this._typed.addConversions(newConversions) - } - } diff --git a/src/core/utils.mjs b/src/core/utils.mjs new file mode 100644 index 0000000..11a879f --- /dev/null +++ b/src/core/utils.mjs @@ -0,0 +1,12 @@ +/* Returns true if set is a subset of the keys of obj */ +export function subsetOfKeys(set, obj) { + for (const e of set) { + if (!(e in obj)) return false + } + return true +} + +/* 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/number/Types/number.mjs b/src/number/Types/number.mjs index 40e390b..417d6f6 100644 --- a/src/number/Types/number.mjs +++ b/src/number/Types/number.mjs @@ -1,7 +1,8 @@ export const Types = { number: { + before: ['Complex'], test: n => typeof n === 'number', - string: s => +s + from: {string: s => +s} } } diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index 1ede574..7f130dc 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -18,10 +18,18 @@ describe('The default full pocomath instance "math"', () => { }) it('can be extended', () => { - math.install({'add': { - '...string': () => addends => addends.reduce((x,y) => x+y, '') - }}) - assert.strictEqual(math.add('Kilroy',' is here'), 'Kilroy is here') + math.install({ + add: { + '...stringK': () => addends => addends.reduce((x,y) => x+y, '') + }, + Types: { + stringK: { + test: s => typeof s === 'string' && s.charAt(0) === 'K', + before: ['string'] + } + } + }) + assert.strictEqual(math.add('Kilroy','K is here'), 'KilroyK is here') }) it('handles complex numbers', () => { diff --git a/test/custom.mjs b/test/custom.mjs index 008218e..4df9d67 100644 --- a/test/custom.mjs +++ b/test/custom.mjs @@ -44,6 +44,23 @@ describe('A custom instance', () => { pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1}) }) + it("can defer definition of (even used) types", () => { + const dt = new PocomathInstance('Deferred Types') + dt.install(numberAdd) + dt.install({times: { + 'number,number': () => (m,n) => m*n, + 'Complex,Complex': ({complex}) => (w,z) => { + return complex(w.re*z.re - w.im*z.im, w.re*z.im + w.im*z.re) + } + }}) + // complex type not present but should still be able to add numbers: + assert.strictEqual(dt.times(3,5), 15) + dt.install(complexComplex) + // times should now rebundle to allow complex: + assert.deepStrictEqual( + dt.times(dt.complex(2,3), dt.complex(2,-3)), dt.complex(13)) + }) + it("can selectively import in cute ways", async function () { const cherry = new PocomathInstance('cherry') cherry.install(numberAdd)