feat: Runtime type reflection #17

Merged
glen merged 21 commits from ts-macros-issues into main 2023-10-17 22:02:18 +00:00
11 changed files with 156 additions and 93 deletions
Showing only changes of commit 6f44567306 - Show all commits

View File

@ -1,6 +1,6 @@
{
name: 'typocomath',
version: '0.0.1',
version: '0.0.2',
description: 'A hopeful final typescipt-pragmatic mathjs proof-of-concept',
main: 'index.ts',
scripts: {

View File

@ -1,9 +1,13 @@
import {Complex} from './type.js'
import type {Dependencies, Signature} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export const isReal =
<T>(dep: Dependencies<'add' | 'equal' | 'isReal', T>):
Signature<'isReal', Complex<T>> =>
z => dep.isReal(z.re) && dep.equal(z.re, dep.add(z.re, z.im))
export const isSquare: Signature<'isSquare', Complex<any>> = z => true // FIXME: not correct for Complex<bigint> once we get there
export const isSquare = (): Signature<'isSquare', Complex<unknown>> =>
z => true // FIXME: not correct for Complex<bigint> once we get there
$reflect!([isReal, isSquare])

View File

@ -1,6 +1,8 @@
import {Complex} from './type.js'
import {Dependencies, Signature} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export const equal =
<T>(dep: Dependencies<'equal', T>): Signature<'equal', Complex<T>> =>
(w, z) => dep.equal(w.re, z.re) && dep.equal(w.im, z.im)
$reflect!([equal])

View File

@ -2,6 +2,7 @@ import {joinTypes, typeOfDependency} from '../core/Dispatcher.js'
import type {
ZeroType, OneType, NaNType, Dependencies, Signature, Returns
} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export type Complex<T> = { re: T; im: T; }
@ -62,3 +63,5 @@ export const nan =
export const re =
<T>(dep: Dependencies<'re', T>): Signature<'re', Complex<T>> =>
z => dep.re(z.re)
$reflect!([complex, zero, one, nan, re])

View File

@ -5,6 +5,8 @@
* for specific types (including their own).
*/
import {parseReflectedType, ImplementationDef} from './parseReflectedType.js'
// First helper types and functions for the Dispatcher
type TypeName = string
@ -39,19 +41,17 @@ type SpecificationsGroup = Record<string, SpecObject>
export class Dispatcher {
installSpecification(
name: string,
signature: Signature,
returns: TypeName,
dependencies: Record<string, Signature>,
defn: ImplementationDef,
behavior: Function // possible todo: constrain this type based
// on the signature, return type, and dependencies. Not sure if
// that's really possible, though.
) {
console.log('Pretending to install', name, signature, '=>', returns)
// @ts-ignore
if (behavior.reflectedType) {
// @ts-ignore
console.log(' Reflected type:', behavior.reflectedType)
// TODO: parse the reflected type
console.log('Pretending to install', name, 'with signatures')
for (const signature of defn.fn.signatures) {
console.log(' ', signature.args, '=>', signature.returns)
}
if (defn.fn.aliasOf) {
console.log(' As an alias of', defn.fn.aliasOf)
}
//TODO: implement me
}
@ -77,7 +77,8 @@ export class Dispatcher {
for (const trio of implementations) {
const [k, id, imp] = trio
console.log('Handling implementation', id, 'from', k)
this.installSpecification(id, ['dunno'], 'unsure', {}, imp)
this.installSpecification(
id, parseReflectedType(id, imp.reflectedType), imp)
}
}
}

View File

@ -1,14 +1,16 @@
export interface FunctionDef {
export type FunctionDef {
name: string,
aliasOf?: string,
signatures: Array<{
args: Array<{ name: string, type: string }>
// FIXME: remove undefined in the end
returns: string | undefined
returns: string
}>
}
export interface DependencyDef extends FunctionDef {
aliasOf: string | undefined
export type ImplementationDef = {
fn: FunctionDef,
dependencies: Record<string, FunctionDef>
}
/**
@ -16,101 +18,116 @@ export interface DependencyDef extends FunctionDef {
*
* '<T>(dep: configDependency & { complex: ((re: number) => Complex<number>) | ((re: number, im: number) => Complex<number>); }) => (a: number) => number | Complex<number>'
*/
export function parseReflectedType(name: string, reflectedType: string) : { fn: FunctionDef, dependencies: Record<string, DependencyDef> } {
const [factoryArgs, fnArgsBlock, returns] = split(reflectedType, '=>').map(trim)
const args = parseArgs(fnArgsBlock)
export function parseReflectedType(name: string, reflectedType: string): ImplementationDef {
console.log('For', name, 'parsing', reflectedType)
const [factoryArgs, fnsClause] = split(reflectedType, '=>', 2).map(trim)
const fn = parseAlias(name, fnsClause)
const factoryArgsInner = findBlockContents(factoryArgs, '(', ')')
const depArg = split(factoryArgsInner.innerText, ':').map(trim)[1]
const depArgBlocks = split(depArg, '&').map(trim)
const depArgBlocks: string[] = depArg ? split(depArg, '&').map(trim) : []
const deps = depArgBlocks
.filter(depArgBlock => {
if (depArgBlock.startsWith('{')) {
if (depArgBlock.startsWith('{') || depArgBlock === 'configDependency') {
return true
} else {
console.error(`ERROR: Cannot parse dependency "${depArgBlock}"`)
throw new SyntaxError(`Cannot parse dependency "${depArgBlock}"`)
}
})
.flatMap(parseDependencies)
const dependencies: Record<string, DependencyDef> = groupBy(deps, 'name')
const dependencies: Record<string, FunctionDef> = groupBy(deps, 'name')
return {
fn: { name, signatures: [{args, returns}] },
dependencies
return {fn, dependencies}
}
function parseDependencies(deps: string): FunctionDef[] {
if (deps === 'configDependency') {
return [{name: 'config', signatures: [{args: [], returns: 'Config'}]}]
}
const inner = findBlockContents(deps, '{', '}').innerText
return split(inner, ';')
.map(trim)
.filter(notEmpty)
.map(parseDependency)
}
function parseDependencies(deps: string) {
const inner = findBlockContents(deps, '{', '}').innerText
return split(inner, ';')
.map(trim)
.filter(notEmpty)
.map(parseDependency)
}
// parse a dependency like "complex: ((re: number) => Complex<number>) | ((re: number, im: number) => Complex<number>)"
function parseDependency(dep: string): FunctionDef {
const [name, def] = split(dep, ':').map(trim)
// parse a dependency like "complex: ((re: number) => Complex<number>) | ((re: number, im: number) => Complex<number>)"
function parseDependency(dep: string) {
const [name, def] = split(dep, ':').map(trim)
return parseAlias(name, def)
}
const { aliasOf, innerSignature } = parseAliasOf(def)
// Parse a possibly aliased function
function parseAlias(name: string, alias: string): FunctionDef {
const { aliasOf, innerSignature } = parseAliasOf(alias)
const signatures = split(innerSignature, '|')
.map(trim)
.map(stripParenthesis)
.map(signature => {
const [argsBlock, returns] = split(signature, '=>').map(trim)
return { name, signatures: parseSignatures(innerSignature), aliasOf }
}
if (!returns) {
console.warn(`ERROR: failed to find return type in '${signature}'`)
}
// parse function signatures like ((re: number) => Complex<number>) | ((re: number, im: number) => Complex<number>)
// But also have to succeed on (a: number) => number | Complex<number>
// That's why we only split on an alternation bar `|` that's followed by
// a parenthesis; that way we avoid splitting a union return type. Note
// this is not necessarily foolproof, as there could be a return type that
// is a union with a complicated piece that has to be enclosed in parens;
// but so far it seems to work in practice.
function parseSignatures(sigs: string) {
return split(sigs, /[|]\s*(?=[(])/)
.map(trim)
.map(stripParenthesis)
.map(signature => {
const [argsBlock, returns] = split(signature, '=>').map(trim)
return {
args: parseArgs(argsBlock),
returns
}
})
return { name, signatures, aliasOf }
}
// parse args like "(re: number, im: number)"
function parseArgs(argsBlock: string) : Array<{name: string, type: string}> {
const args = findBlockContents(argsBlock, '(', ')').innerText
return split(args, ',')
.map(trim)
.map(arg => {
const [name, type] = split(arg, ':').map(trim)
return { name, type}
})
}
// parse "AliasOf<"divide", (a: Complex<T>, b: RealType<Complex<T>>) => Complex<T>>"
function parseAliasOf(signature: string) : { innerSignature: string, aliasOf: string | undefined } {
if (!signature.startsWith('AliasOf')) {
return {
innerSignature: signature,
aliasOf: undefined
if (!returns) {
throw new SyntaxError(`Failed to find return type in '${signature}'`)
}
}
const inner = findBlockContents(signature, '<', '>').innerText.trim()
const [aliasOfWithQuotes, innerSignature] = split(inner, ',').map(trim)
return {
args: parseArgs(argsBlock),
returns
}
})
}
// parse args like "(re: number, im: number)"
function parseArgs(argsBlock: string) : Array<{name: string, type: string}> {
const args = findBlockContents(argsBlock, '(', ')').innerText
return split(args, ',')
.map(trim)
.map(arg => {
const [name, type] = split(arg, ':').map(trim)
return { name, type}
})
}
// parse "AliasOf<"divide", (a: Complex<T>, b: RealType<Complex<T>>) => Complex<T>>"
function parseAliasOf(signature: string) : { innerSignature: string, aliasOf: string | undefined } {
if (!signature.startsWith('AliasOf')) {
return {
innerSignature,
aliasOf: aliasOfWithQuotes.substring(1, aliasOfWithQuotes.length - 1) // remove double quotes
innerSignature: signature,
aliasOf: undefined
}
}
// remove the outer parenthesis, for example "((re: number) => Complex<number>)" returns "(re: number) => Complex<number>"
function stripParenthesis(text: string) : string {
return text.startsWith('(') && text.endsWith(')')
? text.substring(1, text.length - 1)
: text
const inner = findBlockContents(signature, '<', '>').innerText.trim()
const [aliasOfWithQuotes, innerSignature] = split(inner, ',').map(trim)
return {
innerSignature,
aliasOf: aliasOfWithQuotes.substring(1, aliasOfWithQuotes.length - 1) // remove double quotes
}
}
// remove the outer parenthesis, for example "((re: number) => Complex<number>)" returns "(re: number) => Complex<number>"
function stripParenthesis(text: string) : string {
return text.startsWith('(') && text.endsWith(')')
? text.substring(1, text.length - 1)
: text
}
function findBlockContents(text: string, blockStart: string, blockEnd: string, startIndex = 0) : { start: number, end: number, innerText: string } | undefined {
let i = startIndex
@ -143,21 +160,45 @@ function findBlockContents(text: string, blockStart: string, blockEnd: string, s
}
}
/**
* Given a string, generate a string source for a regexp that will match
* exactly the given string.
* Uses the fact that the only characters that need to be escaped in
* a character class are \, ], and ^
*/
function regexpQuote(s: string) {
const special = '\\]^'
let re = ''
for (const char of s) {
if (special.includes(char)) re += `\\${char}`
else re += `[${char}]`
}
return re
}
/**
* Split a string by a delimiter, but ignore all occurrences of the delimiter
* that are inside bracket pairs <> () [] {}
*/
export function split(text: string, delimiter: string) : string[] {
export function split(
text: string, delimiter: string | RegExp, pieces = 0): string[] {
const delim: RegExp = typeof delimiter === 'string'
? new RegExp(regexpQuote(delimiter), 'y')
: new RegExp(delimiter.source, 'y')
const parts: string[] = []
let i = 0
let n = 1
let start = 0
while (i < text.length) {
while (i < text.length && (pieces === 0 || n < pieces)) {
i = skipBrackets(text, i)
if (matchSubString(text, delimiter, i)) {
delim.lastIndex = i
const result = delim.exec(text)
if (result) {
parts.push(text.substring(start, i))
i += delimiter.length
n += 1
i += result[0].length
start = i
}

View File

@ -1,5 +1,7 @@
import {Dependencies, Signature} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export const unequal =
<T>(dep: Dependencies<'equal', T>): Signature<'unequal', T> =>
(x, y) => !dep.equal(x, y)
$reflect!([unequal])

View File

@ -1,2 +1,4 @@
export * from './type.js'
export * from './arithmetic.js'
export * from './predicate.js'
export * from './relational.js'

View File

@ -1,4 +1,7 @@
import type {Signature} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export const isReal: Signature<'isReal', number> = (a) => true
export const isSquare: Signature<'isSquare', number> = (a) => a >= 0
export const isReal = (): Signature<'isReal', number> => (a) => true
export const isSquare = (): Signature<'isSquare', number> => (a) => a >= 0
$reflect!([isReal, isSquare])

View File

@ -1,5 +1,6 @@
import {configDependency} from '../core/Config.js'
import {Signature} from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
const DBL_EPSILON = Number.EPSILON || 2.2204460492503130808472633361816E-16
@ -19,3 +20,4 @@ export const equal =
return false
}
$reflect!([equal])

View File

@ -1,4 +1,5 @@
import type { Signature } from '../interfaces/type.js'
import {$reflect} from '../interfaces/type.js'
export const number_type = {
name: 'number', // just until we have reflection to tell us
@ -20,7 +21,9 @@ declare module "../interfaces/type" {
}
// I don't like the redundancy of repeating 'zero'; any way to eliminate that?
export const zero: Signature<'zero', number> = (a) => 0
export const one: Signature<'one', number> = (a) => 1
export const nan: Signature<'nan', number> = (a) => NaN
export const re: Signature<'re', number> = (a) => a
export const zero = (): Signature<'zero', number> => (a) => 0
export const one = (): Signature<'one', number> => (a) => 1
export const nan = (): Signature<'nan', number> => (a) => NaN
export const re = (): Signature<'re', number> => (a) => a
$reflect!([zero, one, nan, re])