From 1ca0ac42d0b26f5cecf76361efc6634a2c44b1cd Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Thu, 21 Sep 2023 21:01:38 +0200 Subject: [PATCH] feate: implement a basic parser for the reflected types --- src/core/Dispatcher.ts | 8 +- src/core/parseReflectedType.ts | 232 +++++++++++++++++++++++++++++++++ src/index.ts | 26 +++- 3 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 src/core/parseReflectedType.ts diff --git a/src/core/Dispatcher.ts b/src/core/Dispatcher.ts index ec5491f..230a286 100644 --- a/src/core/Dispatcher.ts +++ b/src/core/Dispatcher.ts @@ -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) diff --git a/src/core/parseReflectedType.ts b/src/core/parseReflectedType.ts new file mode 100644 index 0000000..18705fe --- /dev/null +++ b/src/core/parseReflectedType.ts @@ -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: + * + * '(dep: configDependency & { complex: ((re: number) => Complex) | ((re: number, im: number) => Complex); }) => (a: number) => number | Complex' + */ +export function parseReflectedType(name: string, reflectedType: string) : { fn: FunctionDef, dependencies: Record } { + 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 = 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) | ((re: number, im: number) => Complex)" + 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, b: RealType>) => Complex>" + 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)" returns "(re: number) => Complex" + 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(items: T[], key: string) : Record { + const obj: Record = {} + + items.forEach((item) => { + obj[item[key]] = item + }) + + return obj +} diff --git a/src/index.ts b/src/index.ts index dc389e8..5eec64d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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) => 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('(dep: { multiply: (a: T, b: T) => T; }) => (z: T) => T', '=>'))