diff --git a/src/core/PocomathInstance.mjs b/src/core/PocomathInstance.mjs index 398bcee..5755857 100644 --- a/src/core/PocomathInstance.mjs +++ b/src/core/PocomathInstance.mjs @@ -2,7 +2,7 @@ import typed from 'typed-function' import {makeChain} from './Chain.mjs' import {dependencyExtractor, generateTypeExtractor} from './extractors.mjs' -import {R_, returnTypeOf} from './returns.mjs' +import {Returns, returnTypeOf} from './Returns.mjs' import {typeListOfSignature, typesOfSignature, subsetOfKeys} from './utils.mjs' const anySpec = {} // fixed dummy specification of 'any' type @@ -13,7 +13,9 @@ const templateFromParam = 'U' // For defining covariant conversions /* Returns a new signature just like sig but with the parameter replaced by * the type */ -function substituteInSig(sig, parameter, type) { +const upperBounds = /\s*[(]=\s*(\w*)\s*/g +function substituteInSignature(signature, parameter, type) { + const sig = signature.replaceAll(upperBounds, '') const pattern = new RegExp("\\b" + parameter + "\\b", 'g') return sig.replaceAll(pattern, type) } @@ -30,12 +32,14 @@ export default class PocomathInstance { 'install', 'installType', 'instantiateTemplate', + 'isPriorTo', 'isSubtypeOf', 'joinTypes', 'name', 'returnTypeOf', 'self', 'subtypesOf', + 'supertypesOf', 'Templates', 'typeOf', 'Types', @@ -88,7 +92,7 @@ export default class PocomathInstance { this._installFunctions({ typeOf: { - ground: {uses: new Set(), does: () => R_('string', () => 'any')} + ground: {uses: new Set(), does: () => Returns('string', () => 'any')} } }) @@ -202,7 +206,7 @@ export default class PocomathInstance { } if (!(this._typed.isTypedFunction(operation))) return 'any' const details = this._typed.findSignature(operation, signature) - return returnTypeOf(details.fn) + return returnTypeOf(details.fn, signature, this) } /* Return a chain object for this instance with a given value: */ @@ -398,7 +402,7 @@ export default class PocomathInstance { } // update the typeOf function const imp = {} - imp[type] = {uses: new Set(), does: () => R_('string', () => type)} + imp[type] = {uses: new Set(), does: () => Returns('string', () => type)} this._installFunctions({typeOf: imp}) } @@ -412,18 +416,36 @@ export default class PocomathInstance { supSubs.splice(i, 0, sub) } - /* Returns true is typeA is a subtype of type B */ + /* Returns true if typeA is a subtype of type B */ isSubtypeOf(typeA, typeB) { return this._subtypes[typeB].includes(typeA) } + /* Returns true if typeA is a subtype of or converts to type B */ + isPriorTo(typeA, typeB) { + if (!(typeB in this._priorTypes)) return false + return this._priorTypes[typeB].has(typeA) + } + /* Returns a list of the subtypes of a given type, in topological sorted * order (i.e, no type on the list contains one that comes after it). */ subtypesOf(type) { - // HERE! For this to work, have to maintain subtypes as a sorted list. return this._subtypes[type] // should we clone? } + + /* Returns a list of the supertypes of a given type, starting with itself, + * in topological order + */ + supertypesOf(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 @@ -488,7 +510,7 @@ export default class PocomathInstance { const imp = {} imp[type] = { uses: new Set(['T']), - does: ({T}) => R_('string', () => `${base}<${T}>`) + does: ({T}) => Returns('string', () => `${base}<${T}>`) } this._installFunctions({typeOf: imp}) @@ -628,7 +650,7 @@ export default class PocomathInstance { /* Check if it's an ordinary non-template signature */ let explicit = true for (const type of typesOfSignature(rawSignature)) { - for (const word of type.split(/[<>]/)) { + for (const word of type.split(/[<>(=\s]/)) { if (this._templateParam(word)) { explicit = false break @@ -640,14 +662,26 @@ export default class PocomathInstance { 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[1] + 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 (!('instantiations' in behavior)) { behavior.instantiations = new Set() + if (ubType) behavior.instantiations.add(ubType) } let instantiationSet = new Set() for (const instType of behavior.instantiations) { instantiationSet.add(instType) - for (const other of this._priorTypes[instType]) { + for (const other of this.subtypesOf(instType)) { instantiationSet.add(other) } } @@ -656,7 +690,7 @@ export default class PocomathInstance { if (!(instType in this.Types)) continue if (this.Types[instType] === anySpec) continue const signature = - substituteInSig(rawSignature, theTemplateParam, instType) + substituteInSignature(rawSignature, theTemplateParam, instType) /* Don't override an explicit implementation: */ if (signature in imps) continue /* Don't go too deep */ @@ -670,7 +704,7 @@ export default class PocomathInstance { const uses = new Set() for (const dep of behavior.uses) { if (this._templateParam(dep)) continue - uses.add(substituteInSig(dep, theTemplateParam, instType)) + uses.add(substituteInSignature(dep, theTemplateParam, instType)) } const patch = (refs) => { const innerRefs = {} @@ -678,7 +712,7 @@ export default class PocomathInstance { if (this._templateParam(dep)) { innerRefs[dep] = instType } else { - const outerName = substituteInSig( + const outerName = substituteInSignature( dep, theTemplateParam, instType) innerRefs[dep] = refs[outerName] } @@ -689,11 +723,13 @@ export default class PocomathInstance { tf_imps, signature, {uses, does: patch}) } /* Now add the catchall signature */ + /* (Not needed if if it's a bounded template) */ + if (ubType) continue let templateCall = `<${theTemplateParam}>` /* Relying here that the base of 'Foo' is 'Foo': */ let baseSignature = rawSignature.replaceAll(templateCall, '') /* Any remaining template params are top-level */ - const signature = substituteInSig( + const signature = substituteInSignature( baseSignature, theTemplateParam, 'ground') /* The catchall signature has to detect the actual type of the call * and add the new instantiations. @@ -718,7 +754,8 @@ export default class PocomathInstance { for (const dep of behavior.uses) { let [func, needsig] = dep.split(/[()]/) if (needsig) { - const subsig = substituteInSig(needsig, theTemplateParam, '') + const subsig = substituteInSignature( + needsig, theTemplateParam, '') if (subsig === needsig) { simplifiedUses[dep] = dep } else { @@ -789,7 +826,7 @@ export default class PocomathInstance { self._maxDepthSeen = depth } /* Generate the list of actual wanted types */ - const wantTypes = parTypes.map(type => substituteInSig( + const wantTypes = parTypes.map(type => substituteInSignature( type, theTemplateParam, instantiateFor)) /* Now we have to add any actual types that are relevant * to this invocation. Namely, that would be every formal parameter @@ -811,7 +848,7 @@ export default class PocomathInstance { if (wantType.slice(0,3) === '...') { wantType = wantType.slice(3) } - wantType = substituteInSig( + wantType = substituteInSignature( wantType, theTemplateParam, instantiateFor) if (wantType !== parTypes[i]) { args[j] = self._typed.convert(args[j], wantType) @@ -840,7 +877,7 @@ export default class PocomathInstance { } else { let [func, needsig] = dep.split(/[()]/) if (self._typed.isTypedFunction(refs[simplifiedDep])) { - const subsig = substituteInSig( + const subsig = substituteInSignature( needsig, theTemplateParam, instantiateFor) let resname = simplifiedDep if (resname == 'self') resname = name @@ -890,8 +927,8 @@ export default class PocomathInstance { } /* Adapts Pocomath-style behavior specification (uses, does) for signature - * to typed-function implementations and inserts the result into plain object - * imps + * to typed-function implementations and inserts the result into plain + * object imps */ _addTFimplementation(imps, signature, behavior) { const {uses, does} = behavior @@ -910,7 +947,7 @@ export default class PocomathInstance { * Verify that the desired signature has been fully grounded: */ if (needsig) { - const trysig = substituteInSig(needsig, theTemplateParam, '') + const trysig = substituteInSignature(needsig, theTemplateParam, '') if (trysig !== needsig) { throw new Error( 'Attempt to add a template implementation: ' + @@ -1051,8 +1088,9 @@ export default class PocomathInstance { return resultingTypes } - /* Maybe add the instantiation of template type base with argument tyoe - * instantiator to the Types of this instance, if it hasn't happened already. + /* 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). */ @@ -1081,7 +1119,7 @@ export default class PocomathInstance { if (template.before) { for (const beforeTmpl of template.before) { beforeTypes.push( - substituteInSig(beforeTmpl, theTemplateParam, instantiator)) + substituteInSignature(beforeTmpl, theTemplateParam, instantiator)) } } if (beforeTypes.length > 0) { @@ -1090,7 +1128,7 @@ export default class PocomathInstance { newTypeSpec.test = template.test(this._typeTests[instantiator]) if (template.from) { for (let source in template.from) { - const instSource = substituteInSig( + const instSource = substituteInSignature( source, theTemplateParam, instantiator) let usesFromParam = false for (const word of instSource.split(/[<>]/)) { @@ -1101,14 +1139,14 @@ export default class PocomathInstance { } if (usesFromParam) { for (const iFrom in instantiatorSpec.from) { - const finalSource = substituteInSig( + const finalSource = substituteInSignature( instSource, templateFromParam, iFrom) maybeFrom[finalSource] = template.from[source]( instantiatorSpec.from[iFrom]) } // Assuming all templates are covariant here, I guess... for (const subType of this._subtypes[instantiator]) { - const finalSource = substituteInSig( + const finalSource = substituteInSignature( instSource, templateFromParam, subType) maybeFrom[finalSource] = template.from[source](x => x) } diff --git a/src/core/returns.mjs b/src/core/Returns.mjs similarity index 62% rename from src/core/returns.mjs rename to src/core/Returns.mjs index 1c236c4..211babd 100644 --- a/src/core/returns.mjs +++ b/src/core/Returns.mjs @@ -15,15 +15,20 @@ function cloneFunction(fn) { return theClone } -export function R_(type, fn) { +export function Returns(type, fn) { if ('returns' in fn) fn = cloneFunction(fn) fn.returns = type return fn } -export function returnTypeOf(fn) { - return fn.returns || 'any' +export function returnTypeOf(fn, signature, pmInstance) { + const typeOfReturns = typeof fn.returns + if (typeOfReturns === 'undefined') return 'any' + if (typeOfReturns === 'string') return fn.returns + // not sure if we will need a function to determine the return type, + // but allow it for now: + return fn.returns(signature, pmInstance) } -export default R_ +export default Returns diff --git a/src/number/negate.mjs b/src/number/negate.mjs index 5f937a4..696e953 100644 --- a/src/number/negate.mjs +++ b/src/number/negate.mjs @@ -1,37 +1,6 @@ -import R_ from '../core/returns.mjs' +import Returns from '../core/Returns.mjs' export * from './Types/number.mjs' export const negate = { - NumInt: () => R_('NumInt', n => -n), - number: () => R_('number', n => -n) + 'T (= number': ({T}) => Returns(T, n => -n) } -/* Can we find a mechanism to avoid reiterating the definition - for each number (sub)type? Maybe something like: - - export const negate = {'T:number': ({T}) => R_(T, n => -n)} - - where 'T:number' is a new notation that means "T is a (non-strict) subtype - of number". That doesn't look too bad, but if we went down that route, we - might also need to make sure that if we ever implemented a PositiveNumber - type and a NegativeNumber type then we could say - - export const negate = { - PositiveNumber: () => R_('NegativeNumber', n => -n), - NegativeNumber: () => R_('PositiveNumber', n => -n), - 'T:number': ({T}) => R_(T, n => -n) - } - - - This just gets worse if there are also PosNumInt and NegNumInt types; - maybe Positive, Negative, and Zero are template types, so that - we could say: - - export const negate = { - 'Positive': ({'Negative': negT}) => R_(negT, n => -n), - 'Negative': ({'Positive': posT}) => R_(posT, n => -n), - T:number: ({T}) => R_(T, n => -n) - } - - But all of that is pretty speculative, for now let's just go with writing - out the different types. -*/ diff --git a/test/_pocomath.mjs b/test/_pocomath.mjs index c5e8969..63e3b30 100644 --- a/test/_pocomath.mjs +++ b/test/_pocomath.mjs @@ -27,7 +27,7 @@ describe('The default full pocomath instance "math"', () => { it('can subtract numbers', () => { assert.strictEqual(math.subtract(12, 5), 7) - //assert.strictEqual(math.subtract(3n, 1.5), 1.5) + assert.throws(() => math.subtract(3n, 1.5), 'TypeError') }) it('can add numbers', () => {