2022-07-19 00:08:49 +00:00
|
|
|
/* Core of pocomath: create an instance */
|
|
|
|
import typed from 'typed-function'
|
2022-07-23 16:55:02 +00:00
|
|
|
import dependencyExtractor from './dependencyExtractor.mjs'
|
2022-07-23 05:06:48 +00:00
|
|
|
|
2022-07-19 00:08:49 +00:00
|
|
|
export default class PocomathInstance {
|
2022-07-22 21:25:26 +00:00
|
|
|
/* Disallowed names for ops; beware, this is slightly non-DRY
|
|
|
|
* in that if a new top-level PocomathInstance method is added, its name
|
|
|
|
* must be added to this list.
|
|
|
|
*/
|
2022-07-23 02:41:59 +00:00
|
|
|
static reserved = new Set(['install', 'importDependencies'])
|
2022-07-22 21:25:26 +00:00
|
|
|
|
2022-07-19 00:08:49 +00:00
|
|
|
constructor(name) {
|
|
|
|
this.name = name
|
|
|
|
this._imps = {}
|
2022-07-19 18:54:22 +00:00
|
|
|
this._affects = {}
|
2022-07-22 20:49:14 +00:00
|
|
|
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'}])
|
2022-07-19 00:08:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* (Partially) define one or more operations of the instance:
|
|
|
|
*
|
2022-07-23 16:55:02 +00:00
|
|
|
* @param {Object<string, Object<Signature, ({deps})=> implementation>>} ops
|
2022-07-19 00:08:49 +00:00
|
|
|
* The only parameter ops gives the semantics of the operations to install.
|
|
|
|
* The keys are operation names. The value for a key is an object
|
2022-07-23 16:55:02 +00:00
|
|
|
* mapping each desired (typed-function) signature to a function taking
|
|
|
|
* a dependency object to an implementation.
|
2022-07-23 05:06:48 +00:00
|
|
|
*
|
2022-07-23 16:55:02 +00:00
|
|
|
* For more detail, such functions should have the format
|
|
|
|
* ```
|
|
|
|
* ({depA, depB, depC: aliasC, ...}) => (opArg1, opArg2) => <result>
|
|
|
|
* ```
|
|
|
|
* where the `depA`, `depB` etc. are the names of the
|
|
|
|
* operations this implementation depends on; those operations can
|
|
|
|
* then be referred to directly by the identifiers `depA` and `depB`
|
|
|
|
* in the code for the '<result>`, or when an alias has been given
|
|
|
|
* as in the case of `depC`, by the identifier `aliasC`.
|
|
|
|
* Given an object that has these dependencies with these keys, the
|
|
|
|
* function returns a function taking the operation arguments to the
|
|
|
|
* desired result of the operation.
|
2022-07-23 05:06:48 +00:00
|
|
|
*
|
2022-07-23 16:55:02 +00:00
|
|
|
* You can specify that an operation depends on itself by using the
|
|
|
|
* special dependency identifier 'self'.
|
2022-07-23 05:06:48 +00:00
|
|
|
*
|
2022-07-23 16:55:02 +00:00
|
|
|
* You can specify that an implementation depends on just a specific
|
|
|
|
* signature of the given operation by suffixing the dependency name
|
|
|
|
* with the signature in parentheses, e.g. `add(number,number)` to
|
|
|
|
* refer to just adding two numbers. In this case, it is of course
|
|
|
|
* necessary to specify an alias to be able to refer to the supplied
|
|
|
|
* operation in the body of the implementation. [NOTE: this signature-
|
|
|
|
* specific reference is not yet implemented.]
|
2022-07-23 05:06:48 +00:00
|
|
|
*
|
2022-07-22 20:49:14 +00:00
|
|
|
* 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.
|
2022-07-19 00:08:49 +00:00
|
|
|
*/
|
|
|
|
install(ops) {
|
|
|
|
for (const key in ops) this._installOp(key, ops[key])
|
|
|
|
}
|
|
|
|
|
2022-07-23 02:41:59 +00:00
|
|
|
/**
|
|
|
|
* Import (and install) all dependencies of previously installed functions,
|
|
|
|
* for the specified types.
|
|
|
|
*
|
|
|
|
* @param {string[]} types A list of type names
|
|
|
|
*/
|
|
|
|
async importDependencies(types) {
|
|
|
|
const doneSet = new Set(['self']) // nothing to do for self dependencies
|
|
|
|
while (true) {
|
|
|
|
const requiredSet = new Set()
|
|
|
|
/* Grab all of the known deps */
|
|
|
|
for (const func in this._imps) {
|
|
|
|
if (func === 'Types') continue
|
2022-07-23 16:55:02 +00:00
|
|
|
for (const {uses} of Object.values(this._imps[func])) {
|
|
|
|
for (const dependency of uses) {
|
2022-07-23 02:41:59 +00:00
|
|
|
const depName = dependency.split('(',1)[0]
|
|
|
|
if (doneSet.has(depName)) continue
|
|
|
|
requiredSet.add(depName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (requiredSet.size === 0) break
|
|
|
|
for (const name of requiredSet) {
|
|
|
|
for (const type of types) {
|
|
|
|
try {
|
|
|
|
const modName = `../${type}/${name}.mjs`
|
|
|
|
const mod = await import(modName)
|
|
|
|
this.install(mod)
|
|
|
|
} catch (err) {
|
|
|
|
// No such module, but that's OK
|
|
|
|
}
|
|
|
|
}
|
|
|
|
doneSet.add(name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 00:08:49 +00:00
|
|
|
/* Used internally by install, see the documentation there */
|
|
|
|
_installOp(name, implementations) {
|
2022-07-22 21:25:26 +00:00
|
|
|
if (name.charAt(0) === '_') {
|
|
|
|
throw new SyntaxError(
|
|
|
|
`Pocomath: Cannot install ${name}, `
|
|
|
|
+ 'initial _ reserved for internal use.')
|
|
|
|
}
|
|
|
|
if (PocomathInstance.reserved.has(name)) {
|
|
|
|
throw new SyntaxError(
|
|
|
|
`Pocomath: the meaning of function '${name}' cannot be modified.`)
|
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
// new implementations, so set the op up to lazily recreate itself
|
|
|
|
this._invalidate(name)
|
|
|
|
const opImps = this._imps[name]
|
2022-07-23 16:55:02 +00:00
|
|
|
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
|
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
if (signature in opImps) {
|
2022-07-23 16:55:02 +00:00
|
|
|
if (does !== opImps[signature].does) {
|
|
|
|
throw new SyntaxError(
|
|
|
|
`Conflicting definitions of ${signature} for ${name}`)
|
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
} else {
|
2022-07-23 16:55:02 +00:00
|
|
|
if (name === 'Types') {
|
|
|
|
opImps[signature] = does
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const uses = new Set()
|
|
|
|
does(dependencyExtractor(uses))
|
|
|
|
opImps[signature] = {uses, does}
|
|
|
|
for (const dep of uses) {
|
2022-07-19 18:54:22 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset an operation to require creation of typed-function,
|
|
|
|
* and if it has no implementations so far, set them up.
|
|
|
|
*/
|
|
|
|
_invalidate(name) {
|
|
|
|
const self = this
|
2022-07-22 22:06:56 +00:00
|
|
|
Object.defineProperty(this, name, {
|
|
|
|
configurable: true,
|
|
|
|
get: () => self._bundle(name)
|
|
|
|
})
|
2022-07-19 00:08:49 +00:00
|
|
|
if (!(name in this._imps)) {
|
|
|
|
this._imps[name] = {}
|
|
|
|
}
|
2022-07-19 18:54:22 +00:00
|
|
|
if (name in this._affects) {
|
|
|
|
for (const ancestor of this._affects[name]) {
|
|
|
|
this._invalidate(ancestor)
|
|
|
|
}
|
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a typed-function from the signatures for the given name and
|
|
|
|
* assign it to the property with that name, returning it as well
|
|
|
|
*/
|
|
|
|
_bundle(name) {
|
|
|
|
const imps = this._imps[name]
|
|
|
|
if (!imps || Object.keys(imps).length === 0) {
|
|
|
|
throw new SyntaxError(`No implementations for ${name}`)
|
|
|
|
}
|
2022-07-22 20:49:14 +00:00
|
|
|
this._ensureTypes()
|
2022-07-19 00:08:49 +00:00
|
|
|
const tf_imps = {}
|
2022-07-23 16:55:02 +00:00
|
|
|
for (const [signature, {uses, does}] of Object.entries(imps)) {
|
|
|
|
if (uses.length === 0) {
|
|
|
|
tf_imps[signature] = does()
|
2022-07-19 00:08:49 +00:00
|
|
|
} else {
|
2022-07-19 03:10:55 +00:00
|
|
|
const refs = {}
|
2022-07-19 18:54:22 +00:00
|
|
|
let self_referential = false
|
2022-07-23 16:55:02 +00:00
|
|
|
for (const dep of uses) {
|
2022-07-19 03:10:55 +00:00
|
|
|
// TODO: handle signature-specific dependencies
|
|
|
|
if (dep.includes('(')) {
|
|
|
|
throw new Error('signature specific reference unimplemented')
|
|
|
|
}
|
2022-07-19 18:54:22 +00:00
|
|
|
if (dep === 'self') {
|
|
|
|
self_referential = true
|
|
|
|
} else {
|
2022-07-22 22:06:56 +00:00
|
|
|
refs[dep] = this[dep] // assume acyclic for now
|
2022-07-19 18:54:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (self_referential) {
|
2022-07-22 20:49:14 +00:00
|
|
|
tf_imps[signature] = this._typed.referToSelf(self => {
|
2022-07-19 18:54:22 +00:00
|
|
|
refs.self = self
|
2022-07-23 16:55:02 +00:00
|
|
|
return does(refs)
|
2022-07-19 18:54:22 +00:00
|
|
|
})
|
|
|
|
} else {
|
2022-07-23 16:55:02 +00:00
|
|
|
tf_imps[signature] = does(refs)
|
2022-07-19 03:10:55 +00:00
|
|
|
}
|
2022-07-19 00:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
2022-07-22 20:49:14 +00:00
|
|
|
const tf = this._typed(name, tf_imps)
|
2022-07-22 22:06:56 +00:00
|
|
|
Object.defineProperty(this, name, {configurable: true, value: tf})
|
2022-07-19 00:08:49 +00:00
|
|
|
return tf
|
|
|
|
}
|
2022-07-19 03:10:55 +00:00
|
|
|
|
2022-07-22 20:49:14 +00:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
}
|
|
|
|
|
2022-07-19 00:08:49 +00:00
|
|
|
}
|