feat: Template operations (#41)
Relational functions are added using templates, and existing generic functions are made more strict with them. Also a new built-in typeOf function is added, that automatically updates itself. Resolves #34. Co-authored-by: Glen Whitney <glen@studioinfinity.org> Reviewed-on: #41
This commit is contained in:
parent
2609310b8e
commit
fe54bc6004
29 changed files with 480 additions and 119 deletions
|
@ -5,6 +5,16 @@ import {subsetOfKeys, typesOfSignature} from './utils.mjs'
|
|||
|
||||
const anySpec = {} // fixed dummy specification of 'any' type
|
||||
|
||||
const theTemplateParam = 'T' // First pass: only allow this one exact parameter
|
||||
|
||||
/* Returns a new signature just like sig but with the parameter replaced by
|
||||
* the type
|
||||
*/
|
||||
function substituteInSig(sig, parameter, type) {
|
||||
const pattern = new RegExp("\\b" + parameter + "\\b", 'g')
|
||||
return sig.replaceAll(pattern, type)
|
||||
}
|
||||
|
||||
export default class PocomathInstance {
|
||||
/* Disallowed names for ops; beware, this is slightly non-DRY
|
||||
* in that if a new top-level PocomathInstance method is added, its name
|
||||
|
@ -16,6 +26,7 @@ export default class PocomathInstance {
|
|||
'install',
|
||||
'installType',
|
||||
'name',
|
||||
'typeOf',
|
||||
'Types',
|
||||
'undefinedTypes'
|
||||
])
|
||||
|
@ -26,11 +37,22 @@ export default class PocomathInstance {
|
|||
this._affects = {}
|
||||
this._typed = typed.create()
|
||||
this._typed.clear()
|
||||
this.Types = {any: anySpec} // dummy entry to track the default 'any' type
|
||||
/* 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._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.
|
||||
* This is important because if we instantiate a template, we must
|
||||
* instantiate it for all prior types as well, or else the wrong instance
|
||||
* 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._config = {predictable: false}
|
||||
this._config = {predictable: false, epsilon: 1e-12}
|
||||
const self = this
|
||||
this.config = new Proxy(this._config, {
|
||||
get: (target, property) => target[property],
|
||||
|
@ -85,6 +107,20 @@ export default class PocomathInstance {
|
|||
* 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.
|
||||
*
|
||||
* You can specify template implementations. If any item in the signature
|
||||
* contains the word 'T' (currently the only allowed type parameter) then
|
||||
* the signature/implementation is a template. The T can match any type
|
||||
* of argument, and it may appear in the dependencies, where it is
|
||||
* replaced by the matching type. A bare 'T' in the dependencies will be
|
||||
* supplied with the name of the type as its value. See the implementation
|
||||
* of `subtract` for an example.
|
||||
* Usually templates are instantiated as needed, but for some heavily
|
||||
* used functions, or functions with non-template signatures that refer
|
||||
* to signatures generated from a template, it makes more sense to just
|
||||
* instantiate the template immediately for all known types. This eager
|
||||
* instantiation can be accomplished by prefixin the signature with an
|
||||
* exclamation point.
|
||||
*/
|
||||
install(ops) {
|
||||
if (ops instanceof PocomathInstance) {
|
||||
|
@ -130,9 +166,16 @@ export default class PocomathInstance {
|
|||
|
||||
_installInstance(other) {
|
||||
for (const [type, spec] of Object.entries(other.Types)) {
|
||||
if (type === 'any' || this._templateParam(type)) continue
|
||||
this.installType(type, spec)
|
||||
}
|
||||
this._installFunctions(other._imps)
|
||||
const migrateImps = {}
|
||||
for (const operator in other._imps) {
|
||||
if (operator != 'typeOf') { // skip the builtin, we already have it
|
||||
migrateImps[operator] = other._imps[operator]
|
||||
}
|
||||
}
|
||||
this._installFunctions(migrateImps)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,7 +209,12 @@ export default class PocomathInstance {
|
|||
const mod = await import(modName)
|
||||
this.install(mod)
|
||||
} catch (err) {
|
||||
// No such module, but that's OK
|
||||
if (!(err.message.includes('find'))) {
|
||||
// Not just a error because module doesn't exist
|
||||
// So take it seriously
|
||||
throw err
|
||||
}
|
||||
// We don't care if a module doesn't exist, so merely proceed
|
||||
}
|
||||
}
|
||||
doneSet.add(name)
|
||||
|
@ -200,6 +248,10 @@ export default class PocomathInstance {
|
|||
* the corresponding changes to the _typed object immediately
|
||||
*/
|
||||
installType(type, spec) {
|
||||
if (this._templateParam(type)) {
|
||||
throw new SyntaxError(
|
||||
`Type name '${type}' reserved for template parameter`)
|
||||
}
|
||||
if (type in this.Types) {
|
||||
if (spec !== this.Types[type]) {
|
||||
throw new SyntaxError(`Conflicting definitions of type ${type}`)
|
||||
|
@ -227,6 +279,7 @@ export default class PocomathInstance {
|
|||
}
|
||||
this._typed.addTypes([{name: type, test: testFn}], beforeType)
|
||||
this.Types[type] = spec
|
||||
this._priorTypes[type] = new Set()
|
||||
/* Now add conversions to this type */
|
||||
for (const from in (spec.from || {})) {
|
||||
if (from in this.Types) {
|
||||
|
@ -235,6 +288,8 @@ export default class PocomathInstance {
|
|||
while (nextSuper) {
|
||||
this._typed.addConversion(
|
||||
{from, to: nextSuper, convert: spec.from[from]})
|
||||
this._invalidateDependents(':' + nextSuper)
|
||||
this._priorTypes[nextSuper].add(from)
|
||||
nextSuper = this.Types[nextSuper].refines
|
||||
}
|
||||
}
|
||||
|
@ -253,23 +308,30 @@ export default class PocomathInstance {
|
|||
to: nextSuper,
|
||||
convert: this.Types[to].from[type]
|
||||
})
|
||||
this._invalidateDependents(':' + nextSuper)
|
||||
this._priorTypes[nextSuper].add(type)
|
||||
nextSuper = this.Types[nextSuper].refines
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.refines) {
|
||||
this._typed.addConversion(
|
||||
{from: type, to: spec.refines, convert: x => x})
|
||||
}
|
||||
// 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()
|
||||
// Update all the subtype sets of supertypes up the chain:
|
||||
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
|
||||
}
|
||||
// rebundle anything that uses the new type:
|
||||
this._invalidateDependents(':' + type)
|
||||
|
||||
// update the typeOf function
|
||||
const imp = {}
|
||||
imp[type] = {uses: new Set(), does: () => () => type}
|
||||
this._installFunctions({typeOf: imp})
|
||||
}
|
||||
|
||||
/* Returns a list of all types that have been mentioned in the
|
||||
|
@ -292,13 +354,17 @@ export default class PocomathInstance {
|
|||
`Conflicting definitions of ${signature} for ${name}`)
|
||||
}
|
||||
} else {
|
||||
opImps[signature] = behavior
|
||||
// Must avoid aliasing into another instance:
|
||||
opImps[signature] = {uses: behavior.uses, does: behavior.does}
|
||||
for (const dep of behavior.uses) {
|
||||
const depname = dep.split('(', 1)[0]
|
||||
if (depname === 'self') continue
|
||||
if (depname === 'self' || this._templateParam(depname)) {
|
||||
continue
|
||||
}
|
||||
this._addAffect(depname, name)
|
||||
}
|
||||
for (const type of typesOfSignature(signature)) {
|
||||
if (this._templateParam(type)) continue
|
||||
this._usedTypes.add(type)
|
||||
this._addAffect(':' + type, name)
|
||||
}
|
||||
|
@ -307,6 +373,12 @@ export default class PocomathInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/* returns a boolean indicating whether t denotes a template parameter.
|
||||
* We will start this out very simply: the special string `T` is always
|
||||
* a template parameter, and that's the only one
|
||||
*/
|
||||
_templateParam(t) { return t === theTemplateParam }
|
||||
|
||||
_addAffect(dependency, dependent) {
|
||||
if (dependency in this._affects) {
|
||||
this._affects[dependency].add(dependent)
|
||||
|
@ -366,75 +438,177 @@ export default class PocomathInstance {
|
|||
}
|
||||
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 full_self_referential = false
|
||||
let part_self_references = []
|
||||
for (const dep of uses) {
|
||||
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 {
|
||||
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 (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)
|
||||
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
|
||||
}
|
||||
}
|
||||
if (explicit) {
|
||||
this._addTFimplementation(tf_imps, rawSignature, behavior)
|
||||
continue
|
||||
}
|
||||
/* It's a template, have to instantiate */
|
||||
/* First, add the known instantiations, gathering all types needed */
|
||||
if (!('instantiations' in behavior)) {
|
||||
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 instantiationSet) {
|
||||
if (this.Types[instType] === anySpec) continue
|
||||
const signature =
|
||||
substituteInSig(trimSignature, theTemplateParam, instType)
|
||||
/* Don't override an explicit implementation: */
|
||||
if (signature in imps) continue
|
||||
const uses = new Set()
|
||||
for (const dep of behavior.uses) {
|
||||
if (this._templateParam(dep)) continue
|
||||
uses.add(substituteInSig(dep, theTemplateParam, instType))
|
||||
}
|
||||
const patch = (refs) => {
|
||||
const innerRefs = {}
|
||||
for (const dep of behavior.uses) {
|
||||
if (this._templateParam(dep)) {
|
||||
innerRefs[dep] = instType
|
||||
} else {
|
||||
const outerName = substituteInSig(
|
||||
dep, theTemplateParam, instType)
|
||||
innerRefs[dep] = refs[outerName]
|
||||
}
|
||||
}
|
||||
const original = behavior.does(innerRefs)
|
||||
return behavior.does(innerRefs)
|
||||
}
|
||||
this._addTFimplementation(tf_imps, signature, {uses, does: patch})
|
||||
}
|
||||
/* Now add the catchall signature */
|
||||
const signature = substituteInSig(
|
||||
trimSignature, theTemplateParam, 'any')
|
||||
/* The catchall signature has to detect the actual type of the call
|
||||
* and add the new instantiations
|
||||
*/
|
||||
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) {
|
||||
throw new SyntaxError(
|
||||
`Cannot find template parameter in ${rawSignature}`)
|
||||
}
|
||||
const self = this
|
||||
const patch = (refs) => (...args) => {
|
||||
const example = args[exemplar]
|
||||
const instantiateFor = self.typeOf(example)
|
||||
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)
|
||||
}
|
||||
this._addTFimplementation(
|
||||
tf_imps, signature, {uses: behavior.uses, does: patch})
|
||||
}
|
||||
const tf = this._typed(name, tf_imps)
|
||||
Object.defineProperty(this, name, {configurable: true, value: tf})
|
||||
return tf
|
||||
}
|
||||
|
||||
/* Adapts Pocomath-style behavior specification (uses, does) for signature
|
||||
* to typed-function implementations and inserts the result into plain object
|
||||
* imps
|
||||
*/
|
||||
_addTFimplementation(imps, signature, behavior) {
|
||||
const {uses, does} = behavior
|
||||
if (uses.length === 0) {
|
||||
imps[signature] = does()
|
||||
return
|
||||
}
|
||||
const refs = {}
|
||||
let full_self_referential = false
|
||||
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 = ''
|
||||
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 {
|
||||
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 (full_self_referential) {
|
||||
imps[signature] = this._typed.referToSelf(self => {
|
||||
refs.self = self
|
||||
return does(refs)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (part_self_references.length) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
imps[signature] = does(refs)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue