diff --git a/src/Complex/arithmetic.ts b/src/Complex/arithmetic.ts index f6089f4..a1be1f1 100644 --- a/src/Complex/arithmetic.ts +++ b/src/Complex/arithmetic.ts @@ -5,8 +5,10 @@ import type { declare module "../interfaces/type" { interface Operations { - // TODO: Make Dispatcher collapse operations that start with the same - // prefix up to a possible `_` + // TODO: Make Dispatcher collapse operations that match + // after removing any `_...` suffixes; the following should be + // additional dispatches for add and divide, not separate + // operations, in the final mathjs bundle. add_real: {params: [T, RealType], returns: T} divide_real: {params: [T, RealType], returns: T} } @@ -70,12 +72,15 @@ export const divide = OpType<'divide', Complex> => (w, z) => dep.multiply(w, dep.reciprocal(z)) +// The dependencies are slightly tricky here, because there are three types +// involved: Complex, T, and RealType, all of which might be different, +// and we have to get it straight which operations we need on each type, and +// in fact, we need `add_real` on both T and Complex, hence the dependency +// with a custom name, not generated via Dependencies<...> export const sqrt = - (dep: - Dependencies< - 'conservativeSqrt' | 'add' | 'unaryMinus' | 'equal', RealType> - & Dependencies<'zero' | 'add_real', T> - & Dependencies<'complex', T | ZeroType> + (dep: Dependencies<'add' | 'equal' | 'conservativeSqrt' | 'unaryMinus', + RealType> + & Dependencies<'zero' | 'add_real' | 'complex', T> & Dependencies<'absquare' | 're' | 'divide_real', Complex> & {add_complex_real: OpType<'add_real', Complex>}): OpType<'sqrt', Complex> => @@ -84,7 +89,7 @@ export const sqrt = const r = dep.re(z) const negr = dep.unaryMinus(r) if (dep.equal(myabs, negr)) { - // pure imaginary square root; z.im already sero + // pure imaginary square root; z.im already zero return dep.complex( dep.zero(z.re), dep.add_real(z.im, dep.conservativeSqrt(negr))) } diff --git a/src/Complex/type.ts b/src/Complex/type.ts index 8c669ea..379a52a 100644 --- a/src/Complex/type.ts +++ b/src/Complex/type.ts @@ -1,6 +1,4 @@ -import { - joinTypes, typeOfDependency, Dependency, -} from '../core/Dispatcher.js' +import {joinTypes, typeOfDependency} from '../core/Dispatcher.js' import type { ZeroType, OneType, NaNType, Dependencies, OpType, OpReturns } from '../interfaces/type.js' @@ -15,7 +13,7 @@ export const Complex_type = { infer: (dep: typeOfDependency) => (z: Complex) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)), from: { - T: (dep: Dependency<'zero', [T]>) => (t: T) => + T: (dep: Dependencies<'zero', T>) => (t: T) => ({ re: t, im: dep.zero(t) }), Complex: (dep: { convert: (from: U) => T }) => (z: Complex) => ({ re: dep.convert(z.re), im: dep.convert(z.im) }) @@ -39,7 +37,7 @@ declare module "../interfaces/type" { } export const complex = - (dep: Dependencies<'zero', T>): OpType<'complex', T | ZeroType> => + (dep: Dependencies<'zero', T>): OpType<'complex', T> => (a, b) => ({re: a, im: b || dep.zero(a)}) export const zero = diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index 9b005d9..def1bd3 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -9,91 +9,19 @@ type TypeName = string type Parameter = TypeName -type InputSignature = Parameter[] +type Signature = Parameter[] type DependenciesType = Record +// A "canned" dependency for a builtin function: export type typeOfDependency = {typeOf: (x: unknown) => TypeName} -// All of the implementations must publish descriptions of their -// return types into the following interface, using the format -// described just below: -export interface ReturnTypes {} - -/***** - To describe one implementation for a hypothetical operation `foo`, there - should be a property of the interface whose name starts with `foo` and whose - next character, if any, is an underscore. The type of this property - must be the return type of that implementation when Params matches the - parameter types of the implementation, and `never` otherwise. - Thus to describe an implementation that takes a number and a string and - returns a boolean, for example, you could write - ``` - declare module "Dispatcher" { - interface ReturnTypes { - foo_example: Params extends [number, string] ? boolean : never - } - } - ``` - If there is another, generic implementation that takes one argument - of any type and returns a Vector of that type, you can say - ``` - ... - foo_generic: Params extends [infer T] ? Vector : never - ... - ``` - In practice, each subdirectory corresponding to a type, like Complex, - defines an interface, like `ComplexReturn` for the implementations - in that subdirectory, which can mostly be defined without suffixes because - there's typically just a single implementation within that domain. - Then the module responsible for collating all of the implementations for - that type inserts all of the properties of that interface into `ReturnTypes` - suitably suffixed to avoid collisions. - - One might think that simply defining an implementation for `foo` - of type `(n: number, s: string) => boolean` would provide all of the same - information as the type of the key `foo_example` in the ReturnTypes - interface above, but in practice TypeScript has challenges in extracting - types relating to functions. (In particular, there is no - way to get the specialized return type of a generic function when it is - called on aguments whose specific types match the generic parameters.) - Hence the need for this additional mechanism to specify return types, in - a way readily suited for TypeScript type computations. -*****/ - -// Helpers for specifying signatures - -// A basic signature with concrete types -export type Signature = - CandidateParams extends ActualParams ? Returns : never - +// Utility needed in type definitions //dummy implementation for now export function joinTypes(a: TypeName, b: TypeName) { if (a === b) return a return 'any' } -// Used to filter keys that match a given operation name -type BeginsWith = Name | `${Name}_${string}` - -// Look up the return type of an implementation based on its name -// and the parameters it takes -export type ImpReturns = - {[K in keyof ReturnTypes]: K extends BeginsWith - ? ReturnTypes[K] : never}[keyof ReturnTypes] - -// The type of an implementation (with dependencies satisfied, -// based on its name and the parameters it takes -export type ImpType = - (...args: Params) => ImpReturns - -// The type of a dependency on an implementation based on its name -// and the parameters it takes (just a simple object with one property -// named the same as the operation, of value type equal to the type of -// that implementation. These can be `&`ed together in case of multiple -// dependencies: -export type Dependency = - {[N in Name]: ImpType} - // Now types used in the Dispatcher class itself type TypeSpecification = { @@ -110,9 +38,9 @@ type SpecificationsGroup = Record export class Dispatcher { installSpecification( name: string, - signature: InputSignature, + signature: Signature, returns: TypeName, - dependencies: Record, + dependencies: Record, behavior: Function // possible todo: constrain this type based // on the signature, return type, and dependencies. Not sure if // that's really possible, though. diff --git a/src/interfaces/arithmetic.ts b/src/interfaces/arithmetic.ts index 1e4904e..c497b5d 100644 --- a/src/interfaces/arithmetic.ts +++ b/src/interfaces/arithmetic.ts @@ -1,5 +1,5 @@ import type {Complex} from '../Complex/type.js' -import type {RealType, WithConstants, NaNType} from './type.js' +import type {RealType} from './type.js' type UnaryOperator = {params: [T], returns: T} type BinaryOperator = {params: [T, T], returns: T} @@ -17,9 +17,7 @@ declare module "./type" { conservativeSqrt: UnaryOperator sqrt: { params: [T], - returns: T extends Complex - ? Complex> - : T | Complex + returns: T extends Complex ? T : T | Complex } } } diff --git a/src/interfaces/type.ts b/src/interfaces/type.ts index 8460235..b34c68d 100644 --- a/src/interfaces/type.ts +++ b/src/interfaces/type.ts @@ -1,15 +1,18 @@ -// Every typocomath type has some associated types; they need -// to be published as in the following interface. The key is the -// name of the type, and within the subinterface for that key, -// the type of the 'type' property is the actual TypeScript type -// we are associating the other properties to. Note the interface -// is generic with one parameter, corresponding to the fact that -// typocomath currently only allows types with a single generic parameter. -// This way, AssociatedTypes can give the associated types -// for a generic type instantiated with SubType. That's not necessary for -// the 'undefined' type (or if you look in the `numbers` subdirectory, -// the 'number' type either) or any concrete type, but that's OK, the -// generic parameter doesn't hurt in those cases. +/***** + * Every typocomath type has some associated types; they need + * to be published in the following interface. The key is the + * name of the type, and within the subinterface for that key, + * the type of the 'type' property is the actual TypeScript type + * we are associating the other properties to. Note the interface + * is generic with one parameter, corresponding to the fact that + * typocomath currently only allows generic types with a single + * generic parameter. This way, AssociatedTypes can give the + * associated types for a generic type instantiated with SubType. + * That's not necessary for the 'undefined' type (or if you look in the + * `numbers` subdirectory, the 'number' type) or any concrete type, + * but that's OK, the generic parameter doesn't hurt in those cases. + ****/ + export interface AssociatedTypes { undefined: { type: undefined @@ -26,34 +29,41 @@ type ALookup = { T extends AssociatedTypes[K]['type'] ? AssociatedTypes[K][Name] : never }[keyof AssociatedTypes] -export type ZeroType = ALookup -export type OneType = ALookup -export type WithConstants = T | ZeroType | OneType +// For everything to compile, zero and one must be subtypes of T: +export type ZeroType = ALookup & T +export type OneType = ALookup & T +// But I believe 'nan' really might not be, like I think we will have to use +// 'undefined' for the nan of 'bigint', as it has nothing at all like NaN, +// so don't force it: export type NaNType = ALookup export type RealType = ALookup -// The global signature patterns for all operations need to be published in the -// following interface. Each key is the name of an operation (but note that -// the Dispatcher will automatically merge operations that have the same -// name when the first underscore `_` and everything thereafter is stripped). -// The type of each key should be an interface with two properties: 'params' -// whose type is the type of the parameter list for the operation, and -// 'returns' whose type is the return type of the operation on those -// parameters. These types are generic in a parameter type T which should -// be interpreted as the type that the operation is supposed to "primarily" -// operate on, although note that some of the parameters and/or return types -// may depend on T rather than be exactly T. -// So note that the example 're' below provides essentially the same -// information that e.g. -// `type ReOp = (t: T) => RealType` -// would, but in a way that is much easier to manipulate in TypeScript, -// and it records the name of the operation as 're' also by virtue of the -// key 're' in the interface. +/***** + * The global signature patterns for all operations need to be published in the + * following interface. Each key is the name of an operation (but note that + * the Dispatcher will automatically merge operations that have the same + * name when the first underscore `_` and everything thereafter is stripped). + * The type of each key should be an interface with two properties: 'params' + * whose type is the type of the parameter list for the operation, and + * 'returns' whose type is the return type of the operation on those + * parameters. These types are generic in a parameter type T which should + * be interpreted as the type that the operation is supposed to "primarily" + * operate on, although note that some of the parameters and/or return types + * may depend on T rather than be exactly T. + * So note that the example 're' below provides essentially the same + * information that e.g. + * `type ReOp = (t: T) => RealType` + * would, but in a way that is much easier to manipulate in TypeScript, + * and it records the name of the operation as 're' also by virtue of the + * key 're' in the interface. + ****/ export interface Operations { - zero: {params: [WithConstants], returns: ZeroType} - one: {params: [WithConstants], returns: OneType} - nan: {params: [T | NaNType], returns: NaNType} - re: {params: [T], returns: RealType} + zero: {params: [T], returns: ZeroType} + one: {params: [T], returns: OneType} + // nan needs to be able to operate on its own output for everything + // else to compile. That's why its parameter type is widened: + nan: {params: [T | NaNType], returns: NaNType} + re: {params: [T], returns: RealType} } type OpKey = keyof Operations @@ -62,5 +72,3 @@ export type OpReturns = Operations[Name]['returns'] export type OpType = (...args: Operations[Name]['params']) => OpReturns export type Dependencies = {[K in Name]: OpType} - -