Merge pull request 'feat(Types): Improve type spec and ignore signatures that use unknown type' (#23) from forgiving_types into main
Reviewed-on: #23
This commit is contained in:
commit
79f261ff65
@ -1,3 +1,6 @@
|
|||||||
export const Types = {
|
export const Types = {
|
||||||
bigint: {test: b => typeof b === 'bigint'}
|
bigint: {
|
||||||
|
before: ['Complex'],
|
||||||
|
test: b => typeof b === 'bigint'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,10 @@ export function isComplex(z) {
|
|||||||
export const Types = {
|
export const Types = {
|
||||||
Complex: {
|
Complex: {
|
||||||
test: isComplex,
|
test: isComplex,
|
||||||
number: x => ({re: x, im: 0}),
|
from: {
|
||||||
bigint: x => ({re: x, im: 0n})
|
number: x => ({re: x, im: 0}),
|
||||||
|
bigint: x => ({re: x, im: 0n})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* Core of pocomath: create an instance */
|
/* Core of pocomath: create an instance */
|
||||||
import typed from 'typed-function'
|
import typed from 'typed-function'
|
||||||
import dependencyExtractor from './dependencyExtractor.mjs'
|
import dependencyExtractor from './dependencyExtractor.mjs'
|
||||||
|
import {subsetOfKeys, typesOfSignature} from './utils.mjs'
|
||||||
|
|
||||||
export default class PocomathInstance {
|
export default class PocomathInstance {
|
||||||
/* Disallowed names for ops; beware, this is slightly non-DRY
|
/* Disallowed names for ops; beware, this is slightly non-DRY
|
||||||
@ -15,10 +16,9 @@ export default class PocomathInstance {
|
|||||||
this._affects = {}
|
this._affects = {}
|
||||||
this._typed = typed.create()
|
this._typed = typed.create()
|
||||||
this._typed.clear()
|
this._typed.clear()
|
||||||
// Convenient hack for now, would remove when a real string type is added:
|
this.Types = {any: {}} // dummy entry to track the default 'any' type
|
||||||
this._typed.addTypes([{name: 'string', test: s => typeof s === 'string'}])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Partially) define one or more operations of the instance:
|
* (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
|
* Note that the "operation" named `Types` is special: it gives
|
||||||
* types that must be installed in the instance. In this case, the keys
|
* 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'
|
* are type names, and the values are plain objects with the following
|
||||||
* giving the predicate for the type, and properties for each type that can
|
* properties:
|
||||||
* be converted **to** this type, giving the corresponding conversion
|
*
|
||||||
* function.
|
* - 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) {
|
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 */
|
/* Used internally by install, see the documentation there */
|
||||||
_installOp(name, implementations) {
|
_installOp(name, implementations) {
|
||||||
@ -115,42 +164,35 @@ export default class PocomathInstance {
|
|||||||
this._invalidate(name)
|
this._invalidate(name)
|
||||||
const opImps = this._imps[name]
|
const opImps = this._imps[name]
|
||||||
for (const [signature, does] of Object.entries(implementations)) {
|
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 (signature in opImps) {
|
||||||
if (does !== opImps[signature].does) {
|
if (does !== opImps[signature].does) {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
`Conflicting definitions of ${signature} for ${name}`)
|
`Conflicting definitions of ${signature} for ${name}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (name === 'Types') {
|
|
||||||
opImps[signature] = does
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const uses = new Set()
|
const uses = new Set()
|
||||||
does(dependencyExtractor(uses))
|
does(dependencyExtractor(uses))
|
||||||
opImps[signature] = {uses, does}
|
opImps[signature] = {uses, does}
|
||||||
for (const dep of uses) {
|
for (const dep of uses) {
|
||||||
const depname = dep.split('(', 1)[0]
|
const depname = dep.split('(', 1)[0]
|
||||||
if (depname === 'self') continue
|
if (depname === 'self') continue
|
||||||
if (!(depname in this._affects)) {
|
this._addAffect(depname, name)
|
||||||
this._affects[depname] = new Set()
|
}
|
||||||
}
|
for (const type of typesOfSignature(signature)) {
|
||||||
this._affects[depname].add(name)
|
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,
|
* Reset an operation to require creation of typed-function,
|
||||||
* and if it has no implementations so far, set them up.
|
* and if it has no implementations so far, set them up.
|
||||||
@ -177,12 +219,18 @@ export default class PocomathInstance {
|
|||||||
*/
|
*/
|
||||||
_bundle(name) {
|
_bundle(name) {
|
||||||
const imps = this._imps[name]
|
const imps = this._imps[name]
|
||||||
if (!imps || Object.keys(imps).length === 0) {
|
if (!imps) {
|
||||||
throw new SyntaxError(`No implementations for ${name}`)
|
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 = {}
|
const tf_imps = {}
|
||||||
for (const [signature, {uses, does}] of Object.entries(imps)) {
|
for (const [signature, {uses, does}] of usableEntries) {
|
||||||
if (uses.length === 0) {
|
if (uses.length === 0) {
|
||||||
tf_imps[signature] = does()
|
tf_imps[signature] = does()
|
||||||
} else {
|
} else {
|
||||||
@ -214,36 +262,4 @@ export default class PocomathInstance {
|
|||||||
return tf
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
12
src/core/utils.mjs
Normal file
12
src/core/utils.mjs
Normal file
@ -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))
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
export const Types = {
|
export const Types = {
|
||||||
number: {
|
number: {
|
||||||
|
before: ['Complex'],
|
||||||
test: n => typeof n === 'number',
|
test: n => typeof n === 'number',
|
||||||
string: s => +s
|
from: {string: s => +s}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,10 +18,18 @@ describe('The default full pocomath instance "math"', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('can be extended', () => {
|
it('can be extended', () => {
|
||||||
math.install({'add': {
|
math.install({
|
||||||
'...string': () => addends => addends.reduce((x,y) => x+y, '')
|
add: {
|
||||||
}})
|
'...stringK': () => addends => addends.reduce((x,y) => x+y, '')
|
||||||
assert.strictEqual(math.add('Kilroy',' is here'), 'Kilroy is here')
|
},
|
||||||
|
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', () => {
|
it('handles complex numbers', () => {
|
||||||
|
@ -44,6 +44,23 @@ describe('A custom instance', () => {
|
|||||||
pm.subtract({re:5, im:0}, {re:10, im:1}), {re:-5, im: -1})
|
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 () {
|
it("can selectively import in cute ways", async function () {
|
||||||
const cherry = new PocomathInstance('cherry')
|
const cherry = new PocomathInstance('cherry')
|
||||||
cherry.install(numberAdd)
|
cherry.install(numberAdd)
|
||||||
|
Loading…
Reference in New Issue
Block a user