diff --git a/.gitignore b/.gitignore index ceaea36..773a89b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,11 @@ # Logs logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* .pnpm-debug.log* +# Editor backup files +*~ + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -17,116 +16,12 @@ pids *.seed *.pid.lock -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# Compiled code +build # Dependency directories node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ # TypeScript cache *.tsbuildinfo -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - diff --git a/README.md b/README.md index ce4544a..35dd1ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ # math5 -Yet another math core prototype for a possible future of mathjs \ No newline at end of file +Yet another math core prototype for a possible future of mathjs. + +This project is a revision of the +[typocomath](https://code.studioinfinity.org/glen/typocomath) prototype +(which was the fourth in the series picomath, pocomath, typocomath, hence +the current name math5), preparing for an initial implementation of the +Dispatcher engine to assemble and run the methods specified in TypeScript. + +Motivations for the refactor: + +1. I observed that the `.d.ts` files that were being generated as a result +of the TypeScript compilation step did not contain sufficient type information +to see what each implementation/factory for each operation of the resulting +math module would do. This lack suggested that the TypeScript definitions of +the implementations and factories were not actually being fully typechecked. + +2. I felt that there was still a significant amount of redundancy in the +implementation files. For example, in typocomath/src/numbers/arithmetic, it +reiterates for every arithmetic operation "foo" that "foo" implements the +number signature for the "foo" operation. It seemed like it would be +preferable to specify that this module is for the "number" type fewer times, +and not have to mention the operation name for each operation twice. + +3. I did not love the creation of aliased operation names like "addReal" that +were actually implementations of the operation "add" but with different +signatures. I found that mechanism confusing. + +You can verify that the new code compiles and generates implementation +information by cloning the repository and then running `pnpm install` and +`pnpm go`. + +Outcomes of the refactor, corresponding to the motivations: + +1a. You can browse the generated `build/**/*.d.ts` files to see that they +now contain full, detailed type information on every implementation and +factory, including the exact types of the dependencies. + +1b. The TypeScript compiler now correctly detected (which it had not done in +typocomath) that the intermediate real square roots in the complex `sqrt` +implementation might be used even though they had come out to `NaN`. This +outcome is direct evidence that the TypeScript compiler is now type-checking +more strictly, so we are getting greater value from using TypeScript to +specify the operations' behavior. (In addition, it led to adding the `isnan` +predicate so that the code would compile.) + +2. There is less repeated information. For example, +math5/src/numbers/arithmetic only mentions `number` twice, and only mentions +each operation once. + +3. Implementations/factories are now only exported under their actual +operation names, just with different signatures specified. The default name +of a dependency is the name of the operation, but when you have dependencies +on a given operation with different signatures, you can name the dependency +arbitrarily and then specify which operation it is an instance of. + +Other potential advantages of the refactor: + +* Assembling the implementation specifications (the main task of which +is resolving and injecting dependencies) into a running math engine could +potentially work by parsing the `.d.ts` files as generated; we would not +necessarily need to instrument the typescript code with macros to generate +the additional information needed to correctly assemble the factories. + +Some disadvantages of the refactor: + +* The presentation of the code is slightly more verbose in places. The +primary cause of this is the switch to a "builder" interface for collecting +implementations/factories, as advised by TypeScript guru +[jcalz](https://stackoverflow.com/questions/79025259) in order to get +narrower type inference as desired. So for example in +src/Complex/arithmetic, every factory (a "dependent implementation", as +opposed to an "independent" one that has no dependencies) is wrapped in +its own call to `dependent(dependencySpecifiers, factories)`. And that +whole chain of `dependent` calls has to be kicked off with a call to +`implementations()` and wrapped up with a call to `ship()`. Of course, the +names of those functions could be changed, but it appears that currently +there is no way to avoid these wrappers if we want TypeScript to do narrow +type inference/typechecking. + +* When one module is providing multiple implementations for the same +operation, but with different signatures, it must export multiple of these +bundles of implementations generated with an `implementation(). ... .ship()` +seequence, because each bundle can only contain an operation once. The names +of these bundles are arbitrary. I think this artificial division is a little +cumbersome/confusing. See src/Complex/arithmetic for an example, in which +there is a default export with the "common" signatures for operations, and a +`mixed` export with variants for `add` and `divide` that operate on a +Complex and an ordinary number. + +* The notation for the desired signatures for dependencies can still be +a bit arcane/cumbersome. It's very simple when the desired dependency +consists of the common signature for that operation. But for more unusual +situations, it can become intricate. For example, in src/Complex/arithmetic, +the `absquare` (absolute value squared, an operation needed to define +division and square root) factory needs as a dependency the addition +operation on the return type of the `absquare` operation on the base type +of the Complex number. This has ended up being specified as: + +``` +add: {sig: commonSignature<'add', CommonReturn<'absquare', T>>()} +``` + +which is a bit of a mouthful. It's possible that better utilities for +expressing desired signatures could be devised; I'd want to wait until we had +collected a larger number of use cases before trying to design them. (If +this absquare case is essentially a one-off, it doesn't really matter +if it is a bit elaborate.) diff --git a/package.json5 b/package.json5 new file mode 100644 index 0000000..8637870 --- /dev/null +++ b/package.json5 @@ -0,0 +1,31 @@ +{ + name: 'math5', + version: '0.0.1', + description: 'Another prototype for a math core', + scripts: { + build: 'tsc && echo "{\\"type\\": \\"module\\"}" > build/package.json', + go: 'pnpm build && pnpm start', + start: 'node --experimental-loader tsc-module-loader build', + test: 'echo Error no test specified && exit 1', + }, + packageManager: 'pnpm@9', + keywords: [ + 'math', + 'algebra', + 'typescript', + ], + author: 'Glen Whitney', + license: 'Apache 2.0', + repository: { + type: 'git', + url: 'https://code.studioinfinity.org/glen/math5.git', + }, + devDependencies: { + '@types/node': '^22.7.4', + typescript: '^5.6.2', + 'undici-types': '^6.19.8', + }, + dependencies: { + 'tsc-module-loader': '^0.0.1', + }, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..459ca7e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,103 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + tsc-module-loader: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@types/node': + specifier: ^22.7.4 + version: 22.7.4 + typescript: + specifier: ^5.6.2 + version: 5.6.2 + undici-types: + specifier: ^6.19.8 + version: 6.19.8 + +packages: + + '@types/node@22.7.4': + resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + + commonjs-extension-resolution-loader@0.1.0: + resolution: {integrity: sha512-XDCkM/cYIt1CfPs+LNX8nC2KKrzTx5AAlGLpx7A4BjWQCHR9LphDu9Iq5zXYf+PXhCkpLGBFiyiTnwmSnNxbWQ==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tsc-module-loader@0.0.1: + resolution: {integrity: sha512-3SIydFXw96jYU2imgULgIHKlUY8FnfDZlazvNmw4Umx/8qCwXsyDg0V2QOULf2Fw7zaI1Hbibh0mB8VzRZ/Ghg==} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + +snapshots: + + '@types/node@22.7.4': + dependencies: + undici-types: 6.19.8 + + commonjs-extension-resolution-loader@0.1.0: + dependencies: + resolve: 1.22.8 + + function-bind@1.1.2: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + path-parse@1.0.7: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tsc-module-loader@0.0.1: + dependencies: + commonjs-extension-resolution-loader: 0.1.0 + resolve: 1.22.8 + + typescript@5.6.2: {} + + undici-types@6.19.8: {} diff --git a/src/Complex/all.ts b/src/Complex/all.ts new file mode 100644 index 0000000..b0c3344 --- /dev/null +++ b/src/Complex/all.ts @@ -0,0 +1,2 @@ +export * as type_data from './type' +export * as arithmetic_functions from './arithmetic' diff --git a/src/Complex/arithmetic.ts b/src/Complex/arithmetic.ts new file mode 100644 index 0000000..f823b7e --- /dev/null +++ b/src/Complex/arithmetic.ts @@ -0,0 +1,152 @@ +import {Complex} from './type.js' + +import {implementations, commonSpecs} from '@/core/Dispatcher' +import type {RawDependencies} from '@/core/Dispatcher' +import {commonSignature, RealType} from '@/interfaces/type' +import type {CommonSignature, CommonReturn} from '@/interfaces/type' + +// Narrowly typed signature selectors, for the operations we need to use +// with atypical signatures: +const add = 'add' as const +const divide = 'divide' as const + +export default function () { + const baseSignature = commonSpecs() + const withComplex + = (rd: RD) => baseSignature({ + ...rd, complex: {} + }) + + const realSignature = commonSpecs>() + + return implementations>>() + .dependent(withComplex({add: {}}), { + add: dep => (w, z) => + dep.complex(dep.add(w.re, z.re), dep.add(w.im, z.im)) + }) + .dependent(withComplex({unaryMinus: {}}), { + unaryMinus: dep => z => + dep.complex(dep.unaryMinus(z.re), dep.unaryMinus(z.im)) + }) + .dependent(withComplex({unaryMinus: {}, conj: {}}), { + conj: dep => z => dep.complex(dep.conj(z.re), dep.unaryMinus(z.im)) + }) + .dependent(withComplex({subtract: {}}), { + subtract: dep => (w, z) => + dep.complex(dep.subtract(w.re, z.re), dep.subtract(w.re, z.re)) + }) + .dependent(withComplex({add: {}, subtract: {}, multiply: {}, conj: {}}), { + multiply: dep => (w, z) => { + const mult = dep.multiply + const realpart = dep.subtract( + mult( w.re, z.re), mult(dep.conj(w.im), z.im)) + const imagpart = dep.add( + mult(dep.conj(w.re), z.im), mult( w.im, z.re)) + return dep.complex(realpart, imagpart) + } + }) + .dependent(baseSignature({ + absquare: {}, + add: {sig: commonSignature<'add', CommonReturn<'absquare', T>>()} + }), { + absquare: dep => z => dep.add(dep.absquare(z.re), dep.absquare(z.im)) + }) + .dependent({ + conj: {}, + absquare: {}, + divideReal: { + is: divide, + sig: (a: Complex, b: RealType) => ({} as Complex) + } + }, { + reciprocal: dep => z => dep.divideReal(dep.conj(z), dep.absquare(z)) + }) + .dependent({multiply: {}, reciprocal: {}}, { + divide: dep => (w,z) => dep.multiply(w, dep.reciprocal(z)) + }) + // The dependencies are tricky in the implementation of `sqrt` below, + // because there are three types involved: Complex, T, and + // RealType, all of which might be different. + // We have to get it straight which operations we need on each type; + // for example, we need `add` on three different combinations: + .dependent({ + absquare: {}, re: {}, // Complex-dependencies + ...withComplex({zero: {}}), // T-dependencies + // And RealType-dependencies: + ...realSignature({ + conservativeSqrt: {}, equal: {}, unaryMinus: {}, isnan: {} + }), + // And now mixed dependencies: + divideReal: { + is: divide, + sig: (a: Complex, b: RealType) => ({} as Complex) + }, + addRR: { + is: add, + sig: (a: RealType, b: RealType) => ({} as RealType) + }, + addTR: {is: add, sig: (a: T, b: RealType) => ({} as T)}, + addCR: { + is: add, + sig: (a: Complex, b: RealType) => ({} as Complex) + } + }, { + sqrt: dep => z => { + const absq = dep.absquare(z) + const myabs = dep.conservativeSqrt(absq) + if (dep.isnan(myabs)) { + throw new RangeError( + `sqrt(${z}): cannot take square root of norm square ${absq}` + ) + } + const r = dep.re(z) + const negr = dep.unaryMinus(r) + if (dep.equal(myabs, negr)) { + // pure imaginary square root; z.im already zero + const rootNegr = dep.conservativeSqrt(negr) + if (dep.isnan(rootNegr)) { + throw new RangeError( + `sqrt(${z}): cannot take square root of ` + + `negative real part ${negr}` + ) + } + return dep.complex( + dep.zero(z.re), dep.addTR(z.im, rootNegr)) + } + const num = dep.addCR(z, myabs) + const denomsq = dep.addRR(dep.addRR(myabs, myabs), dep.addRR(r, r)) + const denom = dep.conservativeSqrt(denomsq) + if (dep.isnan(denom)) { + throw new RangeError( + `sqrt(z) for z = ${z}: cannot take square root of ` + + `2|z| + 2re(z) = ${denomsq}` + ) + } + return dep.divideReal(num, denom) + } + }) + .ship() +} + +// Additional implementations for non-uniform signatures +export function mixed() { + return implementations<{ + add: (z: Complex, r: RealType) => Complex, + divide: (z: Complex, r: RealType) => Complex, + complex: CommonSignature['complex'] + }>() + .dependent({ + addTR: {is: add, sig: (a: T, b: RealType) => ({} as T)}, + complex: {} + }, { + add: dep => (z, r) => dep.complex(dep.addTR(z.re, r), z.im) + }) + .dependent({ + divTR: {is: divide, sig: (a: T, b: RealType) => ({} as T)}, + complex: {} + }, { + divide: dep => (z, r) => + dep.complex(dep.divTR(z.re, r), dep.divTR(z.im, r)) + }) + .ship() +} diff --git a/src/Complex/type.ts b/src/Complex/type.ts new file mode 100644 index 0000000..e12d275 --- /dev/null +++ b/src/Complex/type.ts @@ -0,0 +1,71 @@ +import {implementations, commonSpecs} from '@/core/Dispatcher' +import {joinTypes} from '@/core/type' + +import type { + CommonInterface, CommonSignature, NaNType, OneType, ZeroType +} from '@/interfaces/type' + +export type Complex = {re: T, im: T} + +export const Complex_type = { + name: 'Complex', + test: (dep: {testT: (z: unknown) => z is T}) => + (z: unknown): z is Complex => + typeof z === 'object' && z != null && 're' in z && 'im' in z + && dep.testT(z.re) && dep.testT(z.im), + infer: (dep: {typeOf: CommonSignature['typeOf']}) => + (z: Complex) => joinTypes(dep.typeOf(z.re), dep.typeOf(z.im)), + from: { + Complex: (dep: {convert: CommonSignature['convert']}) => + (z: Complex) => ({re: dep.convert(z.re), im: dep.convert(z.im)}), + T: (dep: {zero: CommonSignature['zero']}) => + (t: T) => ({re: t, im: dep.zero(t)}) + } +} + +declare module "@/interfaces/type" { + interface AssociatedTypes { + Complex: T extends Complex ? { + type: Complex + zero: Complex> + one: Complex | ZeroType> + nan: Complex> + real: RealType + closure: T + } : never + } + + interface CommonInterface { + complex: (re: T, im?: T) => Complex + } +} + +// internal builder +const cplex = (a:T, b:T): Complex => ({re: a, im: b}) + +export function lift() { + return implementations>() + .dependent({zero: {}}, { + complex: dep => (a, b) => cplex(a, b || dep.zero(a)) + }).ship() +} + +export default function () { + const baseSignature = commonSpecs() + + return implementations>>() + .dependent(baseSignature({zero: {}}), { + zero: dep => z => cplex(dep.zero(z.re), dep.zero(z.re)) + }) + .dependent(baseSignature({zero: {}, one: {}}), { + one: dep => z => + cplex | ZeroType>(dep.one(z.re), dep.zero(z.re)) + }) + .dependent(baseSignature({nan: {}}), { + nan: dep => z => cplex(dep.nan(z.re), dep.nan(z.re)) + }) + .dependent(baseSignature({re: {}}), { + re: dep => z => dep.re(z.re) + }) + .ship() +} diff --git a/src/all.ts b/src/all.ts new file mode 100644 index 0000000..204a957 --- /dev/null +++ b/src/all.ts @@ -0,0 +1,2 @@ +export * as numbers from '@/numbers/all' +export * as Complex from '@/Complex/all' diff --git a/src/core/Configuration.ts b/src/core/Configuration.ts new file mode 100644 index 0000000..fb9c814 --- /dev/null +++ b/src/core/Configuration.ts @@ -0,0 +1,4 @@ +export type Configuration = { + epsilon: number + predictable: boolean +} diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts new file mode 100644 index 0000000..69b90ee --- /dev/null +++ b/src/core/Dispatcher.ts @@ -0,0 +1,201 @@ +import type {AnyFunc, CommonSignature, GenSigs} from '@/interfaces/type' + +// A base type that roughly describes the dependencies of a single factory +// for implementations of one operation. It is an object whose keys are the +// identifiers are dependencies, and whose values describe that dependency. +// In the value for a given key, the 'is' property gives the name of the +// operation that dependency should be an instance of, defaulting to the key +// itself when not present, and the 'sig' property gives the desired +// signature for that operation. When the 'sig' property is not present, +// the signature will default to some ambient ensemble of signatures. +export type RawDependencies = Record + +// The following type transform fills in any unspecified signatures in RD +// with the corresponding signatures from SomeSigs: +type PatchedDepSpec = { + [K in keyof RD]: RD[K] extends {sig: AnyFunc} + ? RD[K] + : K extends keyof SomeSigs ? (RD[K] & {sig: SomeSigs[K]}) : RD[K] +} + +// A factory for building dependency specifications from the ensemble of +// common signatures for a specific type (and perhaps auxiliary type). This +// is typically used when describing implementation factories for one type +// that depend on the common signatures for a *different* type. +export function commonSpecs< + T, + Aux = T, + CommonSigs extends GenSigs = CommonSignature +>() { + return ( + rd: RD + ): PatchedDepSpec => Object.fromEntries( + Object.keys(rd).map(k => [ + k, 'sig' in rd[k] ? rd[k] + : {...rd[k], sig: (() => undefined)} + ]) + ) as PatchedDepSpec +} + +// Further constraint on a dependency specification that means it is ready +// to use with a given set of signatures: +type DepSpec + = { + [K in Needs]: K extends keyof Signatures + ? {sig?: AnyFunc} + : {is: keyof Signatures, sig: AnyFunc} + } + +// Just checks if an RawDependencies is really a DepSpec, and blanks it out if not +type DepCheck< + RD extends RawDependencies, + Signatures extends GenSigs, + Needs extends string = keyof RD & string +> = RD extends DepSpec ? RD + : { + [K in Needs]: K extends keyof Signatures ? {} + : {is: never, sig: (q: boolean) => void} + } + +// The actual type of a dependency, given a dependency specification +type DepType< + Signatures extends GenSigs, + DS extends DepSpec +> = {[K in keyof DS]: DS[K] extends {sig: AnyFunc} + ? DS[K]['sig'] + : K extends keyof Signatures ? Signatures[K] : never +} + +// A collection of dependency specifications for some of the operations in +// an ensemble of Signatures: +type Specifications< + Signatures extends GenSigs, + NeedKeys extends keyof Signatures & string, + NeedList extends Record +> = {[K in NeedKeys]: DepSpec} + +// The type of a factory function for implementations of a dependent operation, +// given a dependency specification: +type FactoryType< + Signatures extends GenSigs, + K extends (keyof Signatures) & string, + DS extends DepSpec +> = (dep: DepType) => Signatures[K] + +// The type of an implementation specification for an operation given its +// dependency specification: either directly the implementation if there +// are actually no dependencies, or a factory function and collection of +// dependency names otherwise: +type ImpType< + Signatures extends GenSigs, + K extends (keyof Signatures) & string, + DS extends DepSpec +> = DS extends null ? {implementation: Signatures[K]} + : {factory: FactoryType, dependencies: DS} + +// A collection of implementations for some operations of an ensemble of +// Signatures, matching a given collection of dependency specifications +type Implementations< + Signatures extends GenSigs, + NeedKeys extends keyof Signatures & string, + NeedList extends Record, + Specs extends Specifications +> = {[K in NeedKeys]: ImpType} + +// The builder interface that lets us assemble narrowly-typed Implementations: +interface ImplementationBuilder< + Signatures extends GenSigs, + NeedKeys extends keyof Signatures & string, + NeedList extends Record, + Specs extends Specifications +> { + independent( + independentImps: {[K in NewKeys]: Signatures[K]} + ): ImplementationBuilder< + Signatures, + NeedKeys | NewKeys, + NeedList & {[K in NewKeys]: never}, + Specs & {[K in NewKeys]: null} + > + + dependent< + RD extends RawDependencies, // Easier to infer + NewKeys extends (keyof Signatures) & string, + DepKeys extends string = keyof RD & string + >( + depSpec: RD, + imps: { + [K in NewKeys]: + FactoryType> + } + ): ImplementationBuilder< + Signatures, + NeedKeys | NewKeys, + NeedList & {[K in NewKeys]: DepKeys}, + Specs & {[K in NewKeys]: DepCheck} + > + + ship(): Implementations +} + +// And a function that actually provides the builder interface: +function impBuilder< + Signatures extends GenSigs, + NeedKeys extends keyof Signatures & string, + NeedList extends Record, + Specs extends Specifications +>( + sofar: Implementations +): ImplementationBuilder { + return { + independent( + imps: {[K in NewKeys]: Signatures[K]}) { + return impBuilder({ + ...sofar, + ...Object.fromEntries(Object.keys(imps).map(k => [k, { + implementation: imps[k] + }])) + } as Implementations< + Signatures, + NeedKeys | NewKeys, + NeedList & {[K in NewKeys]: never}, + Specs & {[K in NewKeys]: null} + >) + }, + + dependent< + RD extends RawDependencies, + NewKeys extends (keyof Signatures) & string, + DepKeys extends string = keyof RD & string + >( + depSpec: RD, + imps: { + [K in NewKeys]: + FactoryType> + } + ) { + return impBuilder({ + ...sofar, + ...Object.fromEntries(Object.keys(imps).map(k => [k, { + factory: imps[k], + dependencies: depSpec + }])) + }) as unknown as ImplementationBuilder< + Signatures, + NeedKeys | NewKeys, + NeedList & {[K in NewKeys]: DepKeys}, + Specs & {[K in NewKeys]: DepCheck} + > + }, + ship() { + return (sofar as + Implementations) + } + } +} + +// A convenience function that gives you an implementation builder: +export function implementations( +): ImplementationBuilder { + return impBuilder({}) +} diff --git a/src/core/type.ts b/src/core/type.ts new file mode 100644 index 0000000..84400fe --- /dev/null +++ b/src/core/type.ts @@ -0,0 +1,7 @@ +import type {TypeName} from '@/interfaces/type' + +// Dummy implementation for now +export function joinTypes(a: TypeName, b: TypeName) { + if (a === b) return a + return 'any' +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..66d4a5b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +import {inspect} from 'node:util' + +import * as specifications from './all' + +console.log(inspect(specifications, {depth: 8, colors: true})) diff --git a/src/interfaces/arithmetic.ts b/src/interfaces/arithmetic.ts new file mode 100644 index 0000000..eedcd75 --- /dev/null +++ b/src/interfaces/arithmetic.ts @@ -0,0 +1,20 @@ +import type {ClosureType, NaNType, RealType} from './type' + +type UnOp = (a: T) => T +type BinOp = (a: T, B: T) => T + +declare module "./type" { + interface CommonInterface { + add: BinOp + unaryMinus: UnOp + conj: UnOp + subtract: BinOp + multiply: BinOp + square: UnOp + absquare: (a: T) => RealType + reciprocal: UnOp + divide: BinOp + conservativeSqrt: (a: T) => (T | NaNType) + sqrt: (a: T) => (T | ClosureType) + } +} diff --git a/src/interfaces/relational.ts b/src/interfaces/relational.ts new file mode 100644 index 0000000..ed6eb82 --- /dev/null +++ b/src/interfaces/relational.ts @@ -0,0 +1,11 @@ +import type {NaNType} from './type.ts' + +export type BinaryPredicate = (a: T, b: T) => boolean + +declare module "./type" { + interface CommonInterface { + equal: BinaryPredicate + unequal: BinaryPredicate + isnan: (a: T | NaNType) => a is NaNType + } +} diff --git a/src/interfaces/type.ts b/src/interfaces/type.ts new file mode 100644 index 0000000..a49da98 --- /dev/null +++ b/src/interfaces/type.ts @@ -0,0 +1,116 @@ +import {Configuration} from '@/core/Configuration' + +/* First some type utilities: */ + +export type ToMapped = {[K in keyof T]: T[K]} + +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) + ? I : never + +export type ValueIntersectionByKeyUnion = { + [P in TKey]: (k: T[P]) => void +} [TKey] extends ((k: infer I) => void) ? I : never + + +// The following needs to be a type that is extended by +// the type of any function. Hopefully I have gotten this right. +export type AnyFunc = (...args: never[]) => unknown + +// An ensemble of signatures of operations, for example, the CommonSignatures +// collected up by all of the modules +export type GenSigs = Record + +/***** + * Every core math 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. To get an associated type, + * types are looked up by matching this 'type' property. + * + * Note the interface is generic with one parameter. To actually find + * the associated types of type T, instantiate the interface with type + * T. This mechanism deals with the fact that TypeScript doesn't really + * deal well with interfaces, some entries of which are generic and others + * are not. + ****/ + +export interface AssociatedTypes { + undefined: { + type: undefined + zero: undefined // The type of the zero of this type + one: undefined // The type of the multiplicative identity of this type + nan: undefined // The type of Not a Number of this type + real: undefined // The type of the real part of this type + closure: undefined // The type of the algebraic closure of this type + } +} + +export type TypeName = string // Really should be some recursive definition, +// any key of AssociatedTypes that's not generic, or any key that is generic +// but instantatied by a TypeName. Not sure how to do that, so just go with +// any string for now. + +type AssociatedTypeNames = keyof AssociatedTypes['undefined'] +type ALookup = + ValueIntersectionByKeyUnion< + {[K in keyof AssociatedTypes]: + T extends AssociatedTypes[K]['type'] + ? AssociatedTypes[K][Name] : unknown + }, + keyof AssociatedTypes + > + +// 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 +// number NaN 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 +export type ClosureType = ALookup + +/***** + * The typical signature for every operation needs to be published in the + * following interface. Each key is the name of an operation. + * The type of each key should be the function type that the operation would + * "normally" have, understanding that there may be exceptions, which will\ + * be dealt with by another mechanism. + * + * Note that this interface is generic in two parameters, the second of which + * defaults to the first. These are slots for type parameters to the typical + * signatures, most of which have only one type parameter. + ****/ + +export interface CommonInterface { + zero: (a: T) => ZeroType + one: (a: T) => OneType + nan: (a: T | NaNType) => NaNType + re: (a: T) => RealType + + config: () => Configuration + convert: (from: T) => Aux + typeOf: (x: unknown) => TypeName +} + +export type CommonSignature = ToMapped> +export type SignatureKey = keyof CommonSignature + +export function commonSignature< + K, T, Aux = T, CS extends GenSigs = CommonSignature +>(): K extends keyof CS ? CS[K] : () => void { + return (() => undefined) as K extends keyof CS ? CS[K] : () => void +} + +export type CommonReturn< + K, T, Aux = T, CS extends GenSigs = CommonSignature +> = K extends keyof CS ? ReturnType : void + +export type Dependency = { + [K in SignatureKey]: Pick, K> +} + +export type Dependencies, Aux = T> = + Pick, Names> diff --git a/src/numbers/all.ts b/src/numbers/all.ts new file mode 100644 index 0000000..b0c3344 --- /dev/null +++ b/src/numbers/all.ts @@ -0,0 +1,2 @@ +export * as type_data from './type' +export * as arithmetic_functions from './arithmetic' diff --git a/src/numbers/arithmetic.ts b/src/numbers/arithmetic.ts new file mode 100644 index 0000000..6f430d6 --- /dev/null +++ b/src/numbers/arithmetic.ts @@ -0,0 +1,26 @@ +import {implementations} from '@/core/Dispatcher' +import type {CommonSignature} from '@/interfaces/type' + +const conservativeSqrt = (a: number) => isNaN(a) ? NaN : Math.sqrt(a) + +export default implementations>() + .independent({ + add: (a, b) => a + b, + unaryMinus: a => -a, + subtract: (a, b) => a - b, + multiply: (a, b) => a * b, + absquare: a => a * a, + reciprocal: a => 1 / a, + divide: (a, b) => a / b, + conj: a => a, + conservativeSqrt }) + .dependent({config: {}, complex: {}}, { + sqrt: dep => { + if (dep.config().predictable || !dep.complex) return conservativeSqrt + return a => { + if (isNaN(a)) return NaN + if (a >= 0) return Math.sqrt(a) + return dep.complex(0, Math.sqrt(-a)) + } + }}) + .ship() diff --git a/src/numbers/type.ts b/src/numbers/type.ts new file mode 100644 index 0000000..c0d0841 --- /dev/null +++ b/src/numbers/type.ts @@ -0,0 +1,31 @@ +import type {Complex} from '@/Complex/type' +import {implementations} from '@/core/Dispatcher' +import type {CommonSignature} from '@/interfaces/type' + +export const number_type = { + name: 'number', + before: ['Complex'], + test: (n: unknown): n is number => typeof n === 'number', + from: {string: (s: string) => +s } +} + +declare module "@/interfaces/type" { + interface AssociatedTypes { + number: { + type: number + zero: 0 + one: 1 + nan: typeof NaN + real: number + closure: Complex + } + } +} + +export default implementations>() + .independent({ + zero: a => 0, + one: a => 1, + nan: a => NaN, + re: a => a + }).ship() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6218fdd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "esnext", + "outDir": "./build", + "paths": { + "@/*": ["./src/*"], + "undici-types": [ + "./node_modules/undici-types/index.d.ts" + ] + }, + "rootDir": "./src", + "target": "esnext", + "types": ["node"] + } +}