2023-10-18 13:11:54 +00:00
|
|
|
export type FunctionDef = {
|
2023-10-17 22:02:18 +00:00
|
|
|
name: string,
|
|
|
|
aliasOf?: string,
|
|
|
|
signatures: Array<{
|
|
|
|
args: Array<{ name: string, type: string }>
|
|
|
|
returns: string
|
|
|
|
}>
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type ImplementationDef = {
|
|
|
|
fn: FunctionDef,
|
|
|
|
dependencies: Record<string, FunctionDef>
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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): 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: string[] = depArg ? split(depArg, '&').map(trim) : []
|
|
|
|
|
|
|
|
const deps = depArgBlocks
|
|
|
|
.filter(depArgBlock => {
|
|
|
|
if (depArgBlock.startsWith('{') || depArgBlock === 'configDependency') {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
throw new SyntaxError(`Cannot parse dependency "${depArgBlock}"`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.flatMap(parseDependencies)
|
|
|
|
|
|
|
|
const dependencies: Record<string, FunctionDef> = groupBy(deps, 'name')
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
return parseAlias(name, def)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse a possibly aliased function
|
|
|
|
function parseAlias(name: string, alias: string): FunctionDef {
|
|
|
|
const { aliasOf, innerSignature } = parseAliasOf(alias)
|
|
|
|
|
|
|
|
return { name, signatures: parseSignatures(innerSignature), aliasOf }
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
if (!returns) {
|
|
|
|
throw new SyntaxError(`Failed to find return type in '${signature}'`)
|
|
|
|
}
|
|
|
|
|
|
|
|
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: 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 | 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 && (pieces === 0 || n < pieces)) {
|
|
|
|
i = skipBrackets(text, i)
|
|
|
|
|
|
|
|
delim.lastIndex = i
|
|
|
|
const result = delim.exec(text)
|
|
|
|
if (result) {
|
|
|
|
parts.push(text.substring(start, i))
|
|
|
|
n += 1
|
|
|
|
i += result[0].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
|
|
|
|
}
|