feat: Runtime type reflection #17

Merged
glen merged 21 commits from ts-macros-issues into main 2023-10-17 22:02:18 +00:00
3 changed files with 261 additions and 5 deletions
Showing only changes of commit 1ca0ac42d0 - Show all commits

View File

@ -47,13 +47,19 @@ export class Dispatcher {
// 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
}
//TODO: implement me
}
installType(name: TypeName, typespec: TypeSpecification) {
console.log('Pretending to install type', name, typespec)
//TODO: implement me
}
constructor(collection: SpecificationsGroup) {
constructor(collection: SpecificationsGroup) {
const implementations = []
for (const key in collection) {
console.log('Working on', key)

View File

@ -0,0 +1,232 @@
export interface FunctionDef {
name: string,
signatures: Array<{
args: Array<{ name: string, type: string }>
// FIXME: remove undefined in the end
returns: string | undefined
}>
}
export interface DependencyDef extends FunctionDef {
aliasOf: string | undefined
}
/**
* Parse a reflected type coming out of TypeScript into a structured object, for example:
*
* '<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)
const factoryArgsInner = findBlockContents(factoryArgs, '(', ')')
const depArg = split(factoryArgsInner.innerText, ':').map(trim)[1]
const depArgBlocks = split(depArg, '&').map(trim)
const deps = depArgBlocks
.filter(depArgBlock => {
if (depArgBlock.startsWith('{')) {
return true
} else {
console.error(`ERROR: Cannot parse dependency "${depArgBlock}"`)
}
})
.flatMap(parseDependencies)
const dependencies: Record<string, DependencyDef> = groupBy(deps, 'name')
return {
fn: { name, signatures: [{args, returns}] },
dependencies
}
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) {
const [name, def] = split(dep, ':').map(trim)
const { aliasOf, innerSignature } = parseAliasOf(def)
const signatures = split(innerSignature, '|')
.map(trim)
.map(stripParenthesis)
.map(signature => {
const [argsBlock, returns] = split(signature, '=>').map(trim)
if (!returns) {
console.warn(`ERROR: failed to find return type in '${signature}'`)
}
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
}
}
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
while (!matchSubString(text, blockStart, i) && i < text.length) {
i++
}
if (i >= text.length) {
return undefined
}
i++
const start = i
while (!matchSubString(text, blockEnd, i) || matchSubString(text, '=>', i - 1)) {
i = skipBrackets(text, i)
i++
}
if (i >= text.length) {
return undefined
}
const end = i
return {
start,
end,
innerText: text.substring(start, end)
}
}
/**
* 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[] {
const parts: string[] = []
let i = 0
let start = 0
while (i < text.length) {
i = skipBrackets(text, i)
if (matchSubString(text, delimiter, i)) {
parts.push(text.substring(start, i))
i += delimiter.length
start = i
}
i++
}
parts.push(text.substring(start))
return parts
}
function skipBrackets(text: string, startIndex: number) : number {
let level = 0
let i = startIndex
do {
if (isBracketOpen(text, i)) {
level++
}
if (isBracketClose(text, i) && level > 0) {
level--
}
if (level === 0) {
break
}
i++
} while(i < text.length)
return i
}
function isBracketOpen(text: string, index: number) {
const char = text[index]
return char === '(' || char === '<' || char === '[' || char === '{'
}
function isBracketClose(text: string, index: number) {
const char = text[index]
// we need to take care of not matching the ">" of the operator "=>"
return char === ')' || (char === '>' && text[index - 1] !== '=') || char === ']' || char === '}'
}
function matchSubString(text: string, search: string, index: number) : boolean {
for (let i = 0; i < search.length; i++) {
if (text[i + index] !== search[i]) {
return false
}
}
return true
}
function trim(text: string) : string {
return text.trim()
}
function notEmpty(text: string) : boolean {
return text.length > 0
}
function groupBy<T>(items: T[], key: string) : Record<string, T> {
const obj: Record<string, T> = {}
items.forEach((item) => {
obj[item[key]] = item
})
return obj
}

View File

@ -1,3 +1,4 @@
import { inspect} from 'node:util'
import {Dispatcher} from './core/Dispatcher.js'
import * as Specifications from './all.js'
@ -5,6 +6,7 @@ export default new Dispatcher(Specifications)
import {Complex} from './Complex/type.js'
import {absquare as absquare_complex} from './Complex/arithmetic.js'
import { parseReflectedType, split } from './core/parseReflectedType.js'
const mockRealAdd = (a: number, b: number) => a+b
const mockComplexAbsquare = (z: Complex<number>) => z.re*z.re + z.im*z.im
@ -31,7 +33,23 @@ const sqrt = Specifications.numbers.sqrt({
console.log('Result of sqrt(16)=', sqrt(16))
console.log('Result of sqrt(-4)=', sqrt(-4))
// Check type of the generic square implementation
console.log('Type of sqrt (number) is', Specifications.numbers.sqrt.reflectedType)
console.log('Type of square is', Specifications.generic.square.reflectedType)
console.log('Type of complex square root is', Specifications.complex.sqrt.reflectedType)
console.log()
console.log('1) NUMBER SQRT')
console.log('1.1) REFLECTED TYPE:', Specifications.numbers.sqrt.reflectedType)
console.log('1.2) PARSED TYPE:', inspect(parseReflectedType('sqrt', Specifications.numbers.sqrt.reflectedType), { depth: null, colors: true }))
console.log()
console.log('2) GENERIC SQUARE')
console.log('2.1) REFLECTED TYPE:', Specifications.generic.square.reflectedType)
console.log('2.2) PARSED TYPE:', inspect(parseReflectedType('square', Specifications.generic.square.reflectedType), { depth: null, colors: true }))
console.log()
console.log('3) COMPLEX SQRT')
console.log('3.1) REFLECTED TYPE:', Specifications.complex.sqrt.reflectedType)
console.log('3.2) PARSED TYPE:', inspect(parseReflectedType('sqrt', Specifications.complex.sqrt.reflectedType), { depth: null, colors: true }))
// FIXME: cleanup
// console.log()
// console.log('split', split('hello**world**how**are**you', '**'))
// console.log('split', split('hello(test**world)**how**are**you', '**'))
// console.log('split', split('<T>(dep: { multiply: (a: T, b: T) => T; }) => (z: T) => T', '=>'))