/* Core of pocomath: create an instance */ import typed from 'typed-function' import {makeChain} from './Chain.mjs' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' import {Returns, returnTypeOf} from './Returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type /* Like `.some(predicate)` but for collections */ function exists(collection, predicate) { for (const item of collection) if (predicate(item)) return true; return false; } /* Template/signature parsing stuff; should probably be moved to a * separate file, but it's a bit interleaved at the moment */ const theTemplateParam = 'T' // First pass: only allow this one exact parameter const restTemplateParam = `...${theTemplateParam}` const templateCall = `<${theTemplateParam}>` const templateFromParam = 'U' // For defining covariant conversions /* returns the pair [base, instance] for a template type. If the type * is not a template, instance is undefined */ const templatePattern = /^\s*([^<\s]*)\s*<\s*(\S*)\s*>\s*$/ function splitTemplate(type) { if (!(type.includes('<'))) return [type, undefined] const results = templatePattern.exec(type) return [results[1], results[2]] } /* Returns the instance such that type is template instantiated for that * instance. */ function whichInstance(type, template) { if (template === theTemplateParam) return type if (type === template) return '' if (!(template.includes(templateCall))) { throw new TypeError( `Type ${template} is not a template, so can't produce ${type}`) } const [typeBase, typeInstance] = splitTemplate(type) if (!typeInstance) { throw new TypeError( `Type ${type} not from a template, so isn't instance of ${template}`) } const [tempBase, tempInstance] = splitTemplate(template) if (typeBase !== tempBase) { throw new TypeError( `Type ${type} has wrong top-level base to be instance of ${template}`) } return whichInstance(typeInstance, tempInstance) } /* Same as above, but for signatures */ function whichSigInstance(sig, tempsig) { const sigTypes = typeListOfSignature(sig) const tempTypes = typeListOfSignature(tempsig) const sigLength = sigTypes.length if (sigLength === 0) { throw new TypeError("No types in signature, so can't determine instance") } if (sigLength !== tempTypes.length) { throw new TypeError(`Signatures ${sig} and ${tempsig} differ in length`) } let maybeInstance = whichInstance(sigTypes[0], tempTypes[0]) for (let i = 1; i < sigLength; ++i) { const currInstance = whichInstance(sigTypes[i], tempTypes[i]) if (maybeInstance) { if (currInstance && currInstance !== maybeInstance) { throw new TypeError( `Inconsistent instantiation of ${sig} from ${tempsig}`) } } else { maybeInstance = currInstance } } if (!maybeInstance) { throw new TypeError( `Signature ${sig} identical to ${tempsig}, not an instance`) } return maybeInstance } /* Returns a new signature just like sig but with the parameter replaced by * the type */ const upperBounds = /\s*(\S*)\s*:\s*(\w*)\s*/g function substituteInSignature(signature, parameter, type) { const sig = signature.replaceAll(upperBounds, '$1') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } const UniversalType = 'ground' // name for a type that matches anything let lastWhatToDo = null // used in an infinite descent check 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 * must be added to this list. */ static reserved = new Set([ 'chain', 'config', 'convert', 'importDependencies', 'install', 'installType', 'instantiateTemplate', 'isPriorTo', 'isSubtypeOf', 'joinTypes', 'name', 'returnTypeOf', 'resolve', 'self', 'subtypesOf', 'supertypesOf', 'Templates', 'typeOf', 'Types', 'undefinedTypes' ]) constructor(name) { this.name = name this._imps = {} // Pocomath implementations, with dependencies this._TFimps = {} // typed-function implementations, dependencies resolved this._affects = {} this._typed = typed.create() this._typed.clear() // The following is an additional typed-function universe for resolving // uninstantiated template instances. It is linked to the main one in // its onMismatch function, below: this._metaTyped = typed.create() this._metaTyped.clear() this._metaTyped.addTypes([{name: UniversalType, test: () => true}]) // And these are the meta bindings: (I think we don't need separate // invalidation for them as they are only accessed through a main call.) this._meta = {} // The resulting typed-functions this._metaTFimps = {} // and their implementations const me = this this._typed.onMismatch = (name, args, sigs) => { if (me._invalid.has(name)) { if (this._fixing === name) { this._fixingCount += 1 if (this._fixingCount > this._maxDepthSeen + 2) { throw new ReferenceError( `Infinite descent rebuilding ${name} on ${args}`) } } else { this._fixingCount = 0 } // rebuild implementation and try again const lastFixing = this._fixing this._fixing = name const value = me[name](...args) this._fix = lastFixing return value } const metaversion = me._meta[name] if (metaversion) { return me._meta[name](...args) } me._typed.throwMismatchError(name, args, sigs) } // 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 // Types that have been moved into the metaverse: this._metafiedTypes = new Set() // All the template types that have been defined: this.Templates = {} // And their instantiations: this._instantiationsOf = {} // The actual type testing functions: this._typeTests = {} // For each type, gives all of its (in)direct subtypes in topo order: this._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._seenTypes = new Set() // all types that have occurred in a signature this._maxDepthSeen = 1 // deepest template nesting we've actually encountered this._invalid = new Set() // methods that are currently invalid this._config = {predictable: false, epsilon: 1e-12} this.config = new Proxy(this._config, { get: (target, property) => target[property], set: (target, property, value) => { if (value !== target[property]) { target[property] = value me._invalidateDependents('config') } return true // successful } }) this._plainFunctions = new Set() // the names of the plain functions this._chainRepository = {} // place to store chainified functions this.joinTypes = this.joinTypes.bind(me) // Provide access to typed function conversion: this.convert = this._typed.convert.bind(this._typed) } /** * (Partially) define one or more operations of the instance: * * The sole parameter can be another Pocomath instance, in which case all * of the types and operations of the other instance are installed in this * one, or it can be a plain object as described below. * * @param {Object implementation>>} ops * The only parameter ops gives the semantics of the operations to install. * The keys are operation names. The value for a key could be * a PocomathInstance, in which case it is simply merged into this * instance. * * Otherwise, ops must be an object * mapping each desired (typed-function) signature to a function taking * a dependency object to an implementation. * * For more detail, such functions should have the format * ``` * ({depA, depB, depC: aliasC, ...}) => (opArg1, opArg2) => * ``` * 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 '`, 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. * * You can specify that an operation depends on itself by using the * special dependency identifier 'self'. * * 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. * * 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 = Returns('void', function(ops) { if (ops instanceof PocomathInstance) { return _installInstance(ops) } /* Standardize the format of all implementations, weeding out * any other instances as we go */ const stdFunctions = {} for (const [item, spec] of Object.entries(ops)) { if (spec instanceof PocomathInstance) { this._installInstance(spec) } else if (typeof spec === 'function') { stdFunctions[item] = spec } else { if (item.charAt(0) === '_') { throw new SyntaxError( `Pocomath: Cannot install ${item}, ` + 'initial _ reserved for internal use.') } if (PocomathInstance.reserved.has(item)) { throw new SyntaxError( `Pocomath: reserved function '${item}' cannot be modified.`) } const stdimps = {} for (const [signature, does] of Object.entries(spec)) { const uses = new Set() try { does(dependencyExtractor(uses)) } catch { } stdimps[signature] = {uses, does} } stdFunctions[item] = stdimps } } this._installFunctions(stdFunctions) }) /* Merge any number of PocomathInstances or modules: */ static merge(name, ...pieces) { const result = new PocomathInstance(name) for (const piece of pieces) { result.install(piece) } return result } /* Determine the return type of an operation given an input signature */ returnTypeOf = Returns('string', function(operation, signature) { for (const type of typeListOfSignature(signature)) { this._maybeInstantiate(type) } if (typeof operation !== 'string') { operation = operation.name } const details = this._pocoFindSignature(operation, signature) if (details) { return returnTypeOf(details.fn, signature, this) } return returnTypeOf(this[operation], signature, this) }) /* Return a chain object for this instance with a given value: */ chain = Returns( sig => `Chain<${sig}>`, function(value) { return makeChain(value, this, this._chainRepository) } ) _installInstance(other) { for (const [type, spec] of Object.entries(other.Types)) { if (spec === anySpec) continue this.installType(type, spec) } for (const [base, info] of Object.entries(other.Templates)) { this._installTemplateType(info.type, info.spec) } const migrateImps = {} for (const operator in other._imps) { if (operator != 'typeOf') { // skip the builtin, we already have it migrateImps[operator] = other._imps[operator] } } for (const plain of other._plainFunctions) { migrateImps[plain] = other[plain] } this._installFunctions(migrateImps) } /** * 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 typeSet = new Set(types) typeSet.add('generic') 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 for (const {uses} of Object.values(this._imps[func])) { for (const dependency of uses) { 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 typeSet) { try { const moduleName = `../${type}/${name}.mjs` const module = await import(moduleName) this.install(module) } catch (err) { 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) } } } /* Used to install a type in a PocomathInstance. * * @param {string} name The name of the type * @param {{test: any => bool, // the predicate for the type * from: Record => > // conversions * before: string[] // lower priority types * refines: string // The type this is a subtype of * }} specification * * The second parameter of this function specifies the structure of the * type via a plain * object 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 * - refines: [optional] the name of a type that this is a subtype * of. This means the test is the conjunction of the given test and * the supertype test, and that it must come before the supertype. */ /* * Implementation note: unlike _installFunctions below, we can make * the corresponding changes to the _typed object immediately */ installType = Returns('void', function(type, spec) { const parts = type.split(/[<,>]/).map(s => s.trim()) if (this._templateParam(parts[0])) { throw new SyntaxError( `Type name '${type}' reserved for template parameter`) } if (parts.some(this._templateParam.bind(this))) { // It's an uninstantiated template, deal with it separately return this._installTemplateType(type, spec) } const base = parts[0] if (type in this.Types) { if (spec !== this.Types[type]) { throw new SyntaxError(`Conflicting definitions of type ${type}`) } return } if (spec.refines && !(spec.refines in this.Types)) { throw new SyntaxError( `Cannot install ${type} before its supertype ${spec.refines}`) } let beforeType = spec.refines if (!beforeType) { beforeType = 'any' for (const other of spec.before || []) { if (other in this.Types) { beforeType = other break } } } let testFn = spec.test if (spec.refines) { const supertypeTest = this.Types[spec.refines].test testFn = entity => supertypeTest(entity) && spec.test(entity) } this._typeTests[type] = testFn this._typed.addTypes([{name: type, test: testFn}], beforeType) this.Types[type] = spec this._subtypes[type] = [] this._priorTypes[type] = new Set() // Update all the subtype sets of supertypes up the chain let nextSuper = spec.refines while (nextSuper) { this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(type) this._addSubtypeTo(nextSuper, type) nextSuper = this.Types[nextSuper].refines } /* Now add conversions to this type */ for (const from in (spec.from || {})) { if (from in this.Types) { // add conversions from "from" to this one and all its supertypes: let nextSuper = type while (nextSuper) { if (this._priorTypes[nextSuper].has(from)) break if (from === nextSuper) break this._typed.addConversion( {from, to: nextSuper, convert: spec.from[from]}) this._invalidateDependents(':' + nextSuper) this._priorTypes[nextSuper].add(from) /* And all of the subtypes of from are now prior as well: */ for (const subtype of this._subtypes[from]) { this._priorTypes[nextSuper].add(subtype) } /* Add the conversion in the metaverse if need be: */ const [toBase, toInstance] = splitTemplate(nextSuper) if (toInstance) { const [fromBase, fromInstance] = splitTemplate(from) if (!fromBase || fromBase !== toBase) { this._metafy(from) try { this._metaTyped.addConversion( {from, to: toBase, convert: spec.from[from]}) } catch { } } } nextSuper = this.Types[nextSuper].refines } } } /* And add conversions from this type */ for (const to in this.Types) { for (const fromtype in this.Types[to].from) { if (type == fromtype || (fromtype in this._subtypes && this.isSubtypeOf(type, fromtype))) { if (spec.refines == to || this.isSubtypeOf(spec.refines,to)) { throw new SyntaxError( `Conversion of ${type} to its supertype ${to} disallowed.`) } let nextSuper = to while (nextSuper) { if (type === nextSuper) break try { // may already be a conversion, and no way to ask this._typed.addConversion({ from: type, to: nextSuper, convert: this.Types[to].from[fromtype] }) this._invalidateDependents(':' + nextSuper) /* Add the conversion in the metaverse if need be: */ const [toBase, toInstance] = splitTemplate(nextSuper) if (toInstance && base !== toBase) { this._metafy(type) this._metaTyped.addConversion({ from: type, to: toBase, convert: this.Types[to].from[fromtype] }) } } catch { } this._priorTypes[nextSuper].add(type) nextSuper = this.Types[nextSuper].refines } } } } // Need to metafy ground types if (type === base) { this._metafy(type) } // update the typeOf function const imp = {} imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) }) _metafy(type) { if (this._metafiedTypes.has(type)) return this._metaTyped.addTypes([{name: type, test: this._typeTests[type]}]) this._metafiedTypes.add(type) } _addSubtypeTo(sup, sub) { if (this.isSubtypeOf(sub, sup)) return const supSubs = this._subtypes[sup] let i for (i = 0; i < supSubs.length; ++i) { if (this.isSubtypeOf(sub, supSubs[i])) break } supSubs.splice(i, 0, sub) } /* Returns true if typeA is a strict subtype of type B */ isSubtypeOf = Returns('boolean', function(typeA, typeB) { // Currently not considering types to be a subtype of 'any' if (typeB === 'any' || typeA === 'any') return false return this._subtypes[typeB].includes(typeA) }) /* Returns true if typeA is a subtype of or converts to type B */ isPriorTo = Returns('boolean', function(typeA, typeB) { if (!(typeB in this._priorTypes)) return false return this._priorTypes[typeB].has(typeA) }) /* Returns a list of the strict ubtypes of a given type, in topological * sorted order (i.e, no type on the list contains one that comes after it). */ subtypesOf = Returns('Array', function(type) { return this._subtypes[type] // should we clone? }) /* Returns a list of the supertypes of a given type, starting with itself, * in topological order */ supertypesOf = Returns('Array', function(type) { const supList = [] while (type) { supList.push(type) type = this.Types[type].refines } return supList }) /* Returns the most refined type containing all the types in the array, * with '' standing for the empty type for convenience. If the second * argument `convert` is true, a convertible type is considered a * a subtype (defaults to false). */ joinTypes = Returns('string', function(types, convert) { let join = '' for (const type of types) { join = this._joinTypes(join, type, convert) } return join }) /* helper for above */ _joinTypes(typeA, typeB, convert) { if (!typeA) return typeB if (!typeB) return typeA if (typeA === 'any' || typeB === 'any') return 'any' if (typeA === typeB) return typeA const subber = convert ? this._priorTypes : this._subtypes const pick = convert ? 'has' : 'includes' if (subber[typeB][pick](typeA)) return typeB /* OK, so we need the most refined supertype of A that contains B: */ let nextSuper = typeA while (nextSuper) { if (subber[nextSuper][pick](typeB)) return nextSuper nextSuper = this.Types[nextSuper].refines } /* And if conversions are allowed, we have to search the other way too */ if (convert) { nextSuper = typeB while (nextSuper) { if (subber[nextSuper][pick](typeA)) return nextSuper nextSuper = this.Types[nextSuper].refines } } return 'any' } /* Returns a list of all types that have been mentioned in the * signatures of operations, but which have not actually been installed: */ undefinedTypes = Returns('Array', function() { return Array.from(this._seenTypes).filter( t => !(t in this.Types || t in this.Templates)) }) /* Used internally to install a template type */ _installTemplateType(type, spec) { const [base] = splitTemplate(type) /* For now, just allow a single template per base type; that * might need to change later: */ if (base in this.Templates) { if (spec !== this.Templates[base].spec) { throw new SyntaxError( `Conflicting definitions of template type ${type}`) } return } // install the "base type" in the meta universe: let beforeType = UniversalType for (const other of spec.before || []) { if (other in this.templates) { beforeType = other break } } this._metaTyped.addTypes([{name: base, test: spec.base}], beforeType) // Add conversions to the base type: if (spec.from && spec.from[theTemplateParam]) { for (const ground of this._metafiedTypes) { this._metaTyped.addConversion( {from: ground, to: base, convert: spec.from[theTemplateParam]}) } } this._instantiationsOf[base] = new Set() // update the typeOf function const imp = {} imp[type] = { uses: new Set(['T']), does: ({T}) => Returns('string', () => `${base}<${T}>`) } this._installFunctions({typeOf: imp}) // Invalidate any functions that reference this template type: this._invalidateDependents(':' + base) // Nothing else actually happens until we match a template parameter this.Templates[base] = {type, spec} } /* Used internally by install, see the documentation there */ _installFunctions(functions) { for (const [name, spec] of Object.entries(functions)) { if (typeof spec === 'function') { if (name in this) { if (spec === this[name]) continue throw new SyntaxError(`Attempt to redefine function ${name}`) } this._plainFunctions.add(name) this[name] = spec continue } // new implementations, first check the name isn't taken if (this._plainFunctions.has(name)) { throw new SyntaxError( `Can't add implementations to function ${name}`) } // All clear, so set the op up to lazily recreate itself this._invalidate(name) const opImps = this._imps[name] for (const [signature, behavior] of Object.entries(spec)) { if (signature in opImps) { if (behavior.does !== opImps[signature].does) { throw new SyntaxError( `Conflicting definitions of ${signature} for ${name}`) } } else { /* Check if it's an ordinary non-template signature */ let explicit = true for (const type of typesOfSignature(signature)) { for (const word of type.split(/[<>:\s]/)) { if (this._templateParam(word)) { explicit = false break } } if (!explicit) break } opImps[signature] = { explicit, resolved: false, uses: behavior.uses, does: behavior.does } if (!explicit) { opImps[signature].hasInstantiations = {} opImps[signature].needsInstantiations = new Set() } for (const dep of behavior.uses) { const depname = dep.split('(', 1)[0] if (depname === 'self' || this._templateParam(depname)) { continue } this._addAffect(depname, name) } for (const type of typesOfSignature(signature)) { for (const word of type.split(/[<>]/)) { if (word.length == 0) continue if (this._templateParam(word)) continue this._seenTypes.add(word) this._addAffect(':' + word, name) } } } } } } /* 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) } 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. * name is the name of the operation, badType is a type that has been * invalidated, and reasons is a set of specific operations/signatures * that have been invalidated */ _invalidate(name, badType = '', reasons = new Set()) { if (!(name in this._imps)) { this._imps[name] = {} this._TFimps[name] = {} this._metaTFimps[name] = {} } // Go through each TF imp and invalidate it if need be for (const [signature, imp] of Object.entries(this._TFimps[name])) { if (imp.deferred || (badType && signature.includes(badType)) || exists(imp.uses, dep => { const [func, sig] = dep.split(/[()]/) return reasons.has(dep) || (reasons.has(func) && !(sig in this._TFimps[func])) })) { // Invalidate this implementation: delete this._TFimps[name][signature] const behavior = imp.fromBehavior if (behavior.explicit) { behavior.resolved = false } else { delete behavior.hasInstantiations[imp.instance] } reasons.add(`${name}(${signature})`) } } if (this._invalid.has(name)) return this._invalid.add(name) this._invalidateDependents(name, badType, reasons) const self = this Object.defineProperty(this, name, { configurable: true, get: () => { const result = self._bundle(name) self._invalid.delete(name) return result } }) } /** * Invalidate all the dependents of a given property of the instance * reasons is a set of invalidated signatures */ _invalidateDependents(name, badType, reasons = new Set()) { if (name.charAt(0) === ':') badType = name.slice(1) else reasons.add(name) if (name in this._affects) { for (const ancestor of this._affects[name]) { this._invalidate(ancestor, badType, reasons) } } } /** * 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) { throw new SyntaxError(`No implementations for ${name}`) } const tf_imps = this._TFimps[name] const meta_imps = this._metaTFimps[name] /* Collect the entries we know the types for */ const usableEntries = [] for (const entry of Object.entries(imps)) { let keep = true for (const type of typesOfSignature(entry[0])) { if (type in this.Types) continue const [baseType] = splitTemplate(type) if (baseType in this.Templates) continue keep = false break } if (keep) usableEntries.push(entry) } if (usableEntries.length === 0) { throw new SyntaxError( `Every implementation for ${name} uses an undefined type;\n` + ` signatures: ${Object.keys(imps)}`) } /* Initial error checking done; mark this method as being * in the midst of being reassembled */ Object.defineProperty(this, name, {configurable: true, value: 'limbo'}) for (const [rawSignature, behavior] of usableEntries) { if (behavior.explicit) { if (!(behavior.resolved)) { this._addTFimplementation(name, tf_imps, rawSignature, behavior) tf_imps[rawSignature]._pocoSignature = rawSignature behavior.resolved = true } continue } /* It's a template, have to instantiate */ /* First, find any upper bounds on the instantation */ /* TODO: handle multiple upper bounds */ upperBounds.lastIndex = 0 let ubType = upperBounds.exec(rawSignature) if (ubType) { ubType = ubType[2] if (!ubType in this.Types) { throw new TypeError( `Unknown type upper bound '${ubType}' in '${rawSignature}'`) } } /* First, add the known instantiations, gathering all types needed */ if (ubType) behavior.needsInstantiations.add(ubType) const nargs = typeListOfSignature(rawSignature).length let instantiationSet = new Set() const ubTypes = new Set() if (!ubType) { // Collect all upper-bound types for this signature for (const othersig in imps) { const otherNargs = typeListOfSignature(othersig).length if (nargs !== otherNargs) { // crude criterion that it won't match, that ignores // rest args, but hopefully OK for prototype continue } const thisUB = upperBounds.exec(othersig) if (thisUB) ubTypes.add(thisUB[2]) let basesig = othersig.replaceAll(templateCall, '') if (basesig !== othersig) { // A template const testsig = substituteInSignature( basesig, theTemplateParam, '') if (testsig === basesig) { // that is not also top-level for (let templateType of typeListOfSignature(basesig)) { if (templateType.slice(0,3) === '...') { templateType = templateType.slice(3) } ubTypes.add(templateType) } } } } } for (const instType of behavior.needsInstantiations) { instantiationSet.add(instType) const otherTypes = ubType ? this.subtypesOf(instType) : this._priorTypes[instType] for (const other of otherTypes) { if (!(this._atOrBelowSomeType(other, ubTypes))) { instantiationSet.add(other) } } } /* Prevent other existing signatures from blocking use of top-level * templates via conversions: */ let baseSignature = rawSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ const signature = substituteInSignature( baseSignature, theTemplateParam, UniversalType) const hasTopLevel = (signature !== baseSignature) if (!ubType && hasTopLevel) { for (const othersig in imps) { let basesig = othersig.replaceAll(templateCall, '') const testsig = substituteInSignature( basesig, theTemplateParam, '') if (testsig !== basesig) continue // a top-level template for (let othertype of typeListOfSignature(othersig)) { if (othertype.slice(0,3) === '...') { othertype = othertype.slice(3) } if (this.Types[othertype] === anySpec) continue const testType = substituteInSignature( othertype, theTemplateParam, '') let otherTypeCollection = [othertype] if (testType !== othertype) { const [base] = splitTemplate(othertype) otherTypeCollection = this._instantiationsOf[base] } for (const possibility of otherTypeCollection) { for (const convtype of this._priorTypes[possibility]) { if (this.isSubtypeOf(convtype, possibility)) continue if (!(this._atOrBelowSomeType(convtype, ubTypes))) { instantiationSet.add(convtype) } } } } } } for (const instType of instantiationSet) { this._instantiateTemplateImplementation( name, rawSignature, instType) } /* Now add the catchall signature */ /* (Not needed if if it's a bounded template) */ if (ubType) continue if (behavior.resolved) continue /* The catchall signature has to detect the actual type of the call * and add the new instantiations. * First, prepare the type inference data: */ const parTypes = rawSignature.split(',') const restParam = (parTypes[parTypes.length-1].slice(0,3) === '...') const topTyper = entity => this.typeOf(entity) const inferences = parTypes.map( type => generateTypeExtractor( type, theTemplateParam, topTyper, this.joinTypes.bind(this), this.Templates)) if (inferences.every(x => !x)) { // all false throw new SyntaxError( `Cannot find template parameter in ${rawSignature}`) } /* Now build the catchall implementation */ const self = this /* For return type annotation, we may have to fix this to propagate the return type. At the moment we are just bagging */ const patch = () => { const patchFunc = (...tfBundledArgs) => { /* We unbundle the rest arg if there is one */ let args = Array.from(tfBundledArgs) const regLength = args.length - 1 if (restParam) { const restArgs = args.pop() args = args.concat(restArgs) } /* Now infer the type we actually should have been called for */ let i = -1 let j = -1 /* collect the arg types */ const argTypes = [] for (const arg of args) { ++j // in case of rest parameter, reuse last parameter type: if (i < inferences.length - 1) ++i if (inferences[i]) { const argType = inferences[i](arg) if (!argType) { throw TypeError( `Type inference failed for argument ${j} of ${name}`) } if (argType === 'any') { console.log('INCOMPATIBLE ARGUMENTS are', args) throw TypeError( `In call to ${name}, ` + 'incompatible template arguments:' // + args.map(a => JSON.stringify(a)).join(', ') // unfortunately barfs on bigints. Need a better // formatter. I wish we could just use the one that // console.log uses; is that accessible somehow? + args.map(a => a.toString()).join(', ') + ' of types ' + argTypes.join(', ') + argType) } argTypes.push(argType) } } if (argTypes.length === 0) { throw TypeError('Type inference failed for' + name) } let usedConversions = false let instantiateFor = self.joinTypes(argTypes) if (instantiateFor === 'any') { usedConversions = true instantiateFor = self.joinTypes(argTypes, usedConversions) if (instantiateFor === 'any') { let argDisplay = args.map(toString).join(', ') throw TypeError( `In call to ${name}, no type unifies arguments ` + argDisplay + '; of types ' + argTypes.toString() + '; note each consecutive pair must unify to a ' + 'supertype of at least one of them') } } const depth = instantiateFor.split('<').length if (depth > self._maxDepthSeen) { self._maxDepthSeen = depth } /* Generate the list of actual wanted types */ const wantTypes = parTypes.map(type => substituteInSignature( type, theTemplateParam, instantiateFor)) const wantSig = wantTypes.join(',') /* Now we have to add any actual types that are relevant * to this invocation. Namely, that would be every formal * parameter type in the invocation, with the parameter * template instantiated by instantiateFor, and for all of * instantiateFor's "prior types" */ for (j = 0; j < parTypes.length; ++j) { if (wantTypes[j] !== parTypes[j] && parTypes[j].includes('<')) { // actually used the param and is a template const strippedType = parTypes[j].substr( parTypes[j].lastIndexOf('.') + 1) self._ensureTemplateTypes(strippedType, instantiateFor) } } /* Request the desired instantiation: */ // But possibly since this resolution was grabbed, the proper // instantiation has been added (like if there are multiple // uses in the implementation of another method. let whatToDo if (!(instantiateFor in behavior.hasInstantiations)) { const newImp = self._instantiateTemplateImplementation( name, rawSignature, instantiateFor) if (newImp) { whatToDo = {fn: newImp, implementation: newImp} } self._invalidate(name) } const brandNewMe = self[name] const betterToDo = self._typed.resolve(brandNewMe, args) whatToDo = betterToDo || whatToDo // We can access return type information here // And in particular, if it might be a template, we should try to // instantiate it: const returnType = returnTypeOf(whatToDo.fn, wantSig, self) for (const possibility of returnType.split('|')) { const instantiated = self._maybeInstantiate(possibility) if (instantiated) { const [tempBase] = splitTemplate(instantiated) self._invalidateDependents(':' + tempBase) } } if (whatToDo === lastWhatToDo) { throw new Error( `Infinite recursion in resolving ${name} called on ` + args.map(x => (typeof x === 'object' ? JSON.stringify(x) : x.toString()) ).join(', ') + ` inferred to be ${wantSig}`) } lastWhatToDo = whatToDo const retval = whatToDo.implementation(...args) lastWhatToDo = null return retval } Object.defineProperty( patchFunc, 'name', {value: `${name}(${signature})`}) patchFunc._pocoSignature = rawSignature return patchFunc } Object.defineProperty( patch, 'name', {value: `generate[${name}(${signature})]`}) // TODO?: Decorate patch with a function that calculates the // correct return type a priori. Deferring because unclear what // aspects will be merged into typed-function. this._addTFimplementation( name, meta_imps, signature, {uses: new Set(), does: patch}, behavior) behavior.resolved = true } // Make sure we have all of the needed (template) types; and if they // can't be added (because they have been instantiated too deep), // ditch the signature: const badSigs = new Set() for (const sig in tf_imps) { if (!tf_imps[sig].uses) { throw new ReferenceError(`MONKEY WRENCH: ${name} ${sig}`) } for (const type of typeListOfSignature(sig)) { if (this._maybeInstantiate(type) === undefined) { badSigs.add(sig) } } } for (const badSig of badSigs) { const imp = tf_imps[badSig] delete tf_imps[badSig] const fromBehavior = this._imps[name][imp._pocoSignature] if (fromBehavior.explicit) { fromBehavior.resolved = false } else { delete fromBehavior.hasInstantiations[imp._pocoInstance] } } let tf if (Object.keys(tf_imps).length > 0) { tf = this._typed(name, tf_imps) tf.fromInstance = this tf.isMeta = false } let metaTF if (Object.keys(meta_imps).length > 0) { metaTF = this._metaTyped(name, meta_imps) metaTF.fromInstance = this metaTF.isMeta = true } this._meta[name] = metaTF tf = tf || metaTF Object.defineProperty(this, name, {configurable: true, value: tf}) return tf } /* Takes a type and a set of types and returns true if the type * is a subtype of some type in the set. */ _atOrBelowSomeType(type, typeSet) { if (typeSet.has(type)) return true let belowSome = false for (const anUB of typeSet) { if (anUB in this.Templates) { if (type.slice(0, anUB.length) === anUB) { belowSome = true break } } else { if (this.isSubtypeOf(type, anUB)) { belowSome = true break } } } return belowSome } /* Takes an arbitrary type and performs an instantiation if necessary. * @param {string} type The type to instantiate * @param {string | bool | undefined } * Returns the name of the type if an instantiation occurred, false * if the type was already present, and undefined if the type can't * be satisfied (because it is not the name of a type or it is nested * too deep. */ _maybeInstantiate(type) { if (type.slice(0,3) === '...') { type = type.slice(3) } if (!(type.includes('<'))) { // Not a template, so just check if type exists if (type in this.Types) return false // already there return undefined // no such type } // it's a template type, turn it into a template and an arg let [base, arg] = splitTemplate(type) return this.instantiateTemplate(base, arg) } /* Generate and include a template instantiation for operation name * for the template signature templateSignature instantiated for * instanceType, returning the resulting implementation. */ _instantiateTemplateImplementation(name, templateSignature, instanceType) { if (!(instanceType in this.Types)) return undefined if (this.Types[instanceType] === anySpec) return undefined const imps = this._imps[name] const behavior = imps[templateSignature] if (instanceType in behavior.hasInstantiations) return undefined const signature = substituteInSignature( templateSignature, theTemplateParam, instanceType) /* Don't override an explicit implementation: */ if (signature in imps) return undefined /* Don't go too deep */ let maxdepth = 0 for (const argType in typeListOfSignature(signature)) { const depth = argType.split('<').length if (depth > maxdepth) maxdepth = depth } if (maxdepth > this._maxDepthSeen + 1) return undefined /* All right, go ahead and instantiate */ const uses = new Set() for (const dep of behavior.uses) { if (this._templateParam(dep)) continue uses.add(substituteInSignature(dep, theTemplateParam, instanceType)) } const patch = (refs) => { const innerRefs = {} for (const dep of behavior.uses) { if (this._templateParam(dep)) { innerRefs[dep] = instanceType } else { const outerName = substituteInSignature( dep, theTemplateParam, instanceType) innerRefs[dep] = refs[outerName] } } return behavior.does(innerRefs) } const tf_imps = this._TFimps[name] this._addTFimplementation( name, tf_imps, signature, {uses, does: patch}, behavior, instanceType) tf_imps[signature]._pocoSignature = templateSignature tf_imps[signature]._pocoInstance = instanceType behavior.hasInstantiations[instanceType] = signature behavior.needsInstantiations.add(instanceType) // once we have it, keep it return tf_imps[signature] } /* Adapts Pocomath-style behavior specification (uses, does) for signature * to typed-function implementations and inserts the result into plain * object imps */ _addTFimplementation( name, imps, signature, specificBehavior, fromImp, asInstance) { if (!fromImp) fromImp = specificBehavior const {uses, does} = specificBehavior if (uses.length === 0) { const implementation = does() implementation.uses = uses implementation.fromInstance = this implementation.fromBehavior = fromImp implementation.instance = asInstance // could do something with return type information here imps[signature] = implementation return } const refs = {} let full_self_referential = false for (const dep of uses) { let [func, needsig] = dep.split(/[()]/) /* Safety check that can perhaps be removed: * Verify that the desired signature has been fully grounded: */ if (needsig) { const trysig = substituteInSignature(needsig, theTemplateParam, '') if (trysig !== needsig) { throw new Error( 'Attempt to add a template implementation: ' + `${signature} with dependency ${dep}`) } } if (func === 'self') { if (needsig) { /* We now resolve all specific-signature self references * here, without resorting to the facility in typed-function: */ if (needsig in imps && typeof imps[needsig] == 'function') { refs[dep] = imps[needsig] continue } const needTypes = typesOfSignature(needsig) const mergedTypes = Object.assign( {}, this.Types, this.Templates) if (subsetOfKeys(needTypes, mergedTypes)) { func = name // just resolve it in limbo } else { // uses an unknown type, so will get an undefined impl console.log( 'WARNING: partial self-reference for', name, 'to', needsig, 'uses an unknown type') refs[dep] = undefined continue } } else { full_self_referential = true continue } } if (this[func] === 'limbo') { /* We are in the midst of bundling func (which may be ourself) */ /* So the first thing we can do is try the tf_imps we are * accumulating: */ if (needsig) { const candidate = this.resolve(func, needsig) if (typeof candidate === 'function') { refs[dep] = candidate continue } } /* Either we need the whole function or the signature * we need is not available yet, so we 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 const redirect = function () { // is this the most efficient? return self[func].apply(this, arguments) } Object.defineProperty(redirect, 'name', {value: func}) Object.defineProperty(redirect, 'fromInstance', {value: this}) refs[dep] = redirect continue } // can bundle up func, and grab its signature if need be let destination = this[func] if (needsig) { destination = this.resolve(func, needsig) } if (!destination) { // Unresolved reference. This is allowed so that // you can bundle up just some portions of the library, // but let's warn. console.log( 'WARNING: No definition found for dependency', dep, 'needed by', name, '(', signature, ')') } refs[dep] = destination } if (full_self_referential) { imps[signature] = this._typed.referToSelf(self => { refs.self = self const implementation = does(refs) Object.defineProperty(implementation, 'name', {value: does.name}) implementation.fromInstance = this implementation.uses = uses implementation.instance = asInstance implementation.fromBehavior = fromImp // What are we going to do with the return type info in here? return implementation }) imps[signature].uses = uses imps[signature].fromInstance = this imps[signature].instance = asInstance imps[signature].fromBehavior = fromImp return } const implementation = does(refs) implementation.fromInstance = this implementation.fromBehavior = fromImp implementation.instance = asInstance implementation.uses = uses // could do something with return type information here? imps[signature] = implementation } /* This function analyzes the template and makes sure the * instantiations of it for type and all prior types of type are present * in the instance. */ _ensureTemplateTypes(template, type) { const [base, arg] = splitTemplate(template) if (!arg) { throw new Error( 'Implementation error in _ensureTemplateTypes', template, type) } let instantiations if (this._templateParam(arg)) { // 1st-level template instantiations = new Set(this._priorTypes[type]) instantiations.add(type) } else { // nested template instantiations = this._ensureTemplateTypes(arg, type) } const resultingTypes = new Set() for (const iType of instantiations) { const resultType = this.instantiateTemplate(base, iType) if (resultType) resultingTypes.add(resultType) } return resultingTypes } /* Maybe add the instantiation of template type base with argument type * instantiator to the Types of this instance, if it hasn't happened * already. * Returns the name of the type if added, false if it was already there, * and undefined if the type is declined (because of being nested too deep). */ instantiateTemplate = Returns('void', function(base, instantiator) { const depth = instantiator.split('<').length if (depth > this._maxDepthSeen ) { // don't bother with types much deeper thant we have seen return undefined } const wantsType = `${base}<${instantiator}>` if (wantsType in this.Types) return false // OK, need to generate the type from the template // Set up refines, before, test, and from const newTypeSpec = {} const maybeFrom = {} const template = this.Templates[base].spec if (!template) { throw new Error( `Implementor error in instantiateTemplate(${base}, ${instantiator})`) } const instantiatorSpec = this.Types[instantiator] if (instantiatorSpec.refines) { this.instantiateTemplate(base, instantiatorSpec.refines) // Assuming our templates are covariant, I guess newTypeSpec.refines = `${base}<${instantiatorSpec.refines}>` } let beforeTypes = [] if (instantiatorSpec.before) { beforeTypes = instantiatorSpec.before.map(type => `${base}<${type}>`) } if (template.before) { for (const beforeTmpl of template.before) { beforeTypes.push( substituteInSignature(beforeTmpl, theTemplateParam, instantiator)) } } if (beforeTypes.length > 0) { newTypeSpec.before = beforeTypes } const templateTest = template.test(this._typeTests[instantiator]) newTypeSpec.test = x => (template.base(x) && templateTest(x)) if (template.from) { for (let source in template.from) { const instSource = substituteInSignature( source, theTemplateParam, instantiator) const testSource = substituteInSignature( instSource, templateFromParam, instantiator) const usesFromParam = (testSource !== instSource) if (usesFromParam) { for (const iFrom in instantiatorSpec.from) { const finalSource = substituteInSignature( instSource, templateFromParam, iFrom) maybeFrom[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } if (testSource !== wantsType) { // subtypes handled with refines // Assuming all templates are covariant here, I guess... for (const subType of this._subtypes[instantiator]) { const finalSource = substituteInSignature( instSource, templateFromParam, subType) maybeFrom[finalSource] = template.from[source](x => x) } } } else { maybeFrom[instSource] = template.from[source] } } } if (Object.keys(maybeFrom).length > 0) { newTypeSpec.from = maybeFrom } this.installType(wantsType, newTypeSpec) this._instantiationsOf[base].add(wantsType) return wantsType }) _findSubtypeImpl(name, imps, neededSig, raw = false) { const detemplate = !raw if (neededSig in imps) return neededSig let foundSig = false const typeList = typeListOfSignature(neededSig) for (const otherSig in imps) { const otherTypeList = typeListOfSignature(otherSig) if (typeList.length !== otherTypeList.length) continue let allMatch = true let paramBound = UniversalType for (let k = 0; k < typeList.length; ++k) { let myType = typeList[k] let otherType = otherTypeList[k] if (otherType === theTemplateParam) { if (detemplate) otherTypeList[k] = paramBound otherType = paramBound } if (otherType === restTemplateParam) { if (detemplate) otherTypeList[k] = `...${paramBound}` otherType = paramBound } const adjustedOtherType = otherType.replaceAll(templateCall, '') if (adjustedOtherType !== otherType) { if (detemplate) otherTypeList[k] = adjustedOtherType otherType = adjustedOtherType } if (myType.slice(0,3) === '...') myType = myType.slice(3) if (otherType.slice(0,3) === '...') otherType = otherType.slice(3) const otherBound = upperBounds.exec(otherType) if (otherBound) { paramBound = otherBound[2] otherType = paramBound if (detemplate) { otherTypeList[k] = otherBound[1].replaceAll( theTemplateParam, paramBound) } } if (otherType === 'any') continue if (otherType === UniversalType) continue if (myType === otherType) continue if (otherType in this.Templates) { const [myBase] = splitTemplate(myType) if (myBase === otherType) continue if (this.instantiateTemplate(otherType, myType)) { let dummy dummy = this[name] // for side effects return this._findSubtypeImpl(name, this._imps[name], neededSig, raw) } } if (!(otherType in this.Types)) { allMatch = false break } if (this.isSubtypeOf(myType, otherType)) continue allMatch = false break } if (allMatch) { foundSig = otherTypeList.join(',') break } } return foundSig } _pocoFindSignature(name, sig, typedFunction) { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] } const haveTF = this._typed.isTypedFunction(typedFunction) && !(typedFunction.isMeta) if (haveTF) { // First try a direct match let result try { result = this._typed.findSignature(typedFunction, sig, {exact: true}) } catch { } if (result) return result // Next, look ourselves but with subtypes: const wantTypes = typeListOfSignature(sig) for (const [implSig, details] of typedFunction._typedFunctionData.signatureMap) { let allMatched = true const implTypes = typeListOfSignature(implSig) if (implTypes.length > wantTypes.length) { // Not enough arguments for that implementation continue } for (let i = 0; i < wantTypes.length; ++i) { const implIndex = Math.min(i, implTypes.length - 1) let implType = implTypes[implIndex] if (implIndex < i) { if (implType.slice(0,3) !== '...') { // ran out of arguments in impl allMatched = false break } } if (implType.slice(0,3) === '...') { implType = implType.slice(3) } const hasMatch = implType.split('|').some( t => (wantTypes[i] === t || this.isSubtypeOf(wantTypes[i], t))) if (!hasMatch) { allMatched = false break } } if (allMatched) return details } } if (!(this._imps[name])) return undefined const foundsig = this._findSubtypeImpl(name, this._imps[name], sig, 'raw') if (foundsig) { if (haveTF) { try { return this._typed.findSignature(typedFunction, foundsig) } catch { } } const instantiationMatcher = '^' + substituteInSignature(foundsig, theTemplateParam, '(.*)') .replaceAll(UniversalType, '(.*)') + '$' const instanceMatch = sig.match(instantiationMatcher) let possibleInstantiator = false if (instanceMatch) { possibleInstantiator = instanceMatch[1] for (let i = 2; i < instanceMatch.length; ++i) { if (possibleInstantiator !== instanceMatch[i]) { possibleInstantiator = false break } } } if (possibleInstantiator) { const behavior = this._imps[name][foundsig] let newInstance if (behavior) { if (!(possibleInstantiator in behavior.hasInstantiations)) { newInstance = this._instantiateTemplateImplementation( name, foundsig, possibleInstantiator) } else { // OK, so we actually have the instantiation. Let's get it newInstance = this._TFimps[name][sig] } // But we may not have taken advantage of conversions this._invalidate(name) const tryAgain = this[name] let betterInstance if (this._typed.isTypedFunction(tryAgain)) { betterInstance = this._typed.findSignature(tryAgain, sig) } if (betterInstance) { newInstance = betterInstance } else { newInstance = { fn: newInstance, implementation: newInstance } } if (newInstance) return newInstance } } const catchallSig = this._findSubtypeImpl(name, this._imps[name], sig) if (catchallSig !== foundsig) { try { return this._metaTyped.findSignature( this._meta[name], catchallSig) } catch { } } // We have an implementation but not a typed function. Do the best // we can: const restoredSig = foundsig.replaceAll('ground', theTemplateParam) const foundImpl = this._imps[name][restoredSig] const needs = {} for (const dep of foundImpl.uses) { const [base, sig] = dep.split(/[()]/) if (sig) { needs[dep] = this.resolve(base, sig) } else { needs[dep] = this[dep] } } const pseudoImpl = foundImpl.does(needs) pseudoImpl.fromInstance = this return {fn: pseudoImpl, implementation: pseudoImpl} } // Hmm, no luck. Make sure bundle is up-to-date and retry: let result = undefined typedFunction = this[name] try { result = this._typed.findSignature(typedFunction, sig) } catch { } return result } /* Returns a function that implements the operation with the given name * when called with the given signature. The optional third argument is * the typed function that provides the operation name, which can be * passed in for efficiency if it is already available. */ resolve = Returns('function', function (name, sig, typedFunction) { if (!this._typed.isTypedFunction(typedFunction)) { typedFunction = this[name] } const result = this._pocoFindSignature(name, sig, typedFunction) if (result) return result.implementation // total punt, revert to typed-function resolution on every call; // hopefully this happens rarely: return typedFunction }) }