diff --git a/src/control-flow/cfg-defs.ts b/src/control-flow/cfg-defs.ts index 4fa1144..6befb5b 100644 --- a/src/control-flow/cfg-defs.ts +++ b/src/control-flow/cfg-defs.ts @@ -33,6 +33,7 @@ export type NodeType = | "SWITCH_CONDITION" | "SWITCH_MERGE" | "CASE_CONDITION"; + export type EdgeType = "regular" | "consequence" | "alternative" | "exception"; export type ClusterType = @@ -64,28 +65,94 @@ export interface GraphEdge { type: EdgeType; } +/** + * Represents a `goto` statement. + */ export interface Goto { + /** + * The name of the label to jump to + */ label: string; + /** + * The node containing the `goto` statement + */ node: string; } +/** + * The `BasicBlock` is used in the process of building a CFG. + * It is meant to represent a "statement" or "block" of source-code. + * + * It allows us to abstract over individual CFG nodes and work with something + * that's closer to the source code. + * + * `BasicBlocks`s can be nested, just like code structures. + * If we have a `BasicBlock` representing an `if` statement, it will have + * `BasicBlock`s for the individual statements nested within it. + * + * Each `BasicBlock` can span many CFG nodes. + */ export interface BasicBlock { + /** + * The ID of the entry node. + */ entry: string; + /** + * Normal exit node, if exists. + * + * This would be the next statement after the current one in the source code. + * For control-flow statements (like `return`, `break`, etc.) we won't have + * an `exit`, as the control-flow doesn't reach it. + * Similarly, blocks ending with a flow-altering statement won't have an exit. + */ exit: string | null; + /** + * The active `continue` nodes within this block. + * + * A `continue` is active if it's target is outside the current block. + */ continues?: string[]; + /** + * The active `break` nodes within this block. + */ breaks?: string[]; - // From label name to node + /** + * All the labels within this block. + * + * The mapping is from the label's name to the labeled node. + */ labels?: Map; - // Target label + /** + * All the active `goto`s within this block. + * + * Active `goto`s are one that were not yet resolved. + */ gotos?: Goto[]; - // Return statements in the block. Needed for exception handling. + /** + * All the `return` statements within the block. + * As the CFG is a single-function graph, `return` statements are never + * resolved within it. + */ returns?: string[]; } export type CFGGraph = MultiDirectedGraph; + +/** + * The full CFG structure. + */ export interface CFG { + /** + * The graph itself + */ graph: CFGGraph; + /** + * The function's entry node + */ entry: string; + /** + * A mapping between source-code offsets and their matching CFG nodes + */ offsetToNode: SimpleRange[]; } @@ -93,12 +160,12 @@ export class BlockHandler { private breaks: string[] = []; private continues: string[] = []; private labels: Map = new Map(); - private gotos: Array<{ label: string; node: string }> = []; + private gotos: Array = []; /** * All the returns encountered so far. * * This is needed for `finally` clauses in exception handling, - * as the return is moved/duplicated to the end of the finally clause. + * as the return is moved/duplicated to the end of the finally-clause. * This means that when processing returns, we expect to get a new set * of returns. */ diff --git a/src/control-flow/generic-cfg-builder.ts b/src/control-flow/generic-cfg-builder.ts index 9a7eed3..e2c358a 100644 --- a/src/control-flow/generic-cfg-builder.ts +++ b/src/control-flow/generic-cfg-builder.ts @@ -39,9 +39,9 @@ export interface Context { /** * A function that converts an AST node to a CFG basic-block. * - * @param {Parser.SyntaxNode} syntax - The AST node to be processed - * @param {Context} ctx - The context in which the statement is being handled - * @returns {BasicBlock} A basic block representation of the AST node + * @param syntax The AST node to be processed + * @param ctx The context in which the statement is being handled + * @returns A basic block representation of the AST node */ export type StatementHandler = ( syntax: Parser.SyntaxNode, diff --git a/src/control-flow/graph-ops.ts b/src/control-flow/graph-ops.ts index a78c4e3..f9c7f04 100644 --- a/src/control-flow/graph-ops.ts +++ b/src/control-flow/graph-ops.ts @@ -15,18 +15,35 @@ export function distanceFromEntry(cfg: CFG): Map { return levels; } -/// Can return null to indicate that the merge is not allowed. -/// The function MUST NOT modify the input arguments. +/** + * Merges the attributes of two nodes, or aborts the merge. + * + * The function MUST NOT modify the input arguments. + * + * @param nodeAttrs the node to disappear + * @param intoAttrs the node accepting the new attributes + * @returns the new node attribute if the merge is successful, or `null` to abort it. + */ export type AttrMerger = ( nodeAttrs: GraphNode, intoAttrs: GraphNode, ) => GraphNode | null; + +/** + * Collapses one node into another, migrating all relevant edges and merging + * the node attributes. + * + * @param graph The graph to collapse in + * @param node Node to collapse + * @param into Collapse target + * @param mergeAttrs Controls the merger of attributes. Can prevent collapsing. + */ function collapseNode( graph: MultiDirectedGraph, node: string, into: string, mergeAttrs?: AttrMerger, -) { +): void { if (mergeAttrs) { const attrs = mergeAttrs( graph.getNodeAttributes(node), @@ -55,8 +72,10 @@ function collapseNode( graph.dropNode(node); } /** + * Simplify the CFG by removing "trivial" nodes. * - * @param cfg The graph to simplify + * Trivial nodes are nodes that do not contribute to the branching structure of + * the CFG. */ export function simplifyCFG(cfg: CFG, mergeAttrs?: AttrMerger): CFG { const graph = cfg.graph.copy(); @@ -90,6 +109,12 @@ export function simplifyCFG(cfg: CFG, mergeAttrs?: AttrMerger): CFG { return evolve(cfg, { graph, entry }); } +/** + * Remove all nodes not reachable from the CFG's entry + * + * @param cfg The CFG to trim + * @returns a copy of the CFG, with the unreachable nodes removed + */ export function trimFor(cfg: CFG): CFG { const { graph, entry } = cfg; const reachable: string[] = []; diff --git a/src/control-flow/render.ts b/src/control-flow/render.ts index 0718de8..0056be1 100644 --- a/src/control-flow/render.ts +++ b/src/control-flow/render.ts @@ -309,7 +309,7 @@ function renderEdge( // For backlinks, we use `dir=back` to improve the layout. // This tells DOT that this is a backlink, and changes the ranking of nodes. dotAttrs.dir = "back"; - // To accomodate that, we also flip the node order and the ports. + // To accommodate that, we also flip the node order and the ports. [source, target] = [target, source]; dotAttrs.headport = "s"; dotAttrs.tailport = "n"; diff --git a/src/control-flow/zip.ts b/src/control-flow/zip.ts index a6f3610..6f802d0 100644 --- a/src/control-flow/zip.ts +++ b/src/control-flow/zip.ts @@ -1,13 +1,47 @@ +/** + * Zips multiple arrays together, creating tuples of elements at corresponding indices. + * Stops iterating when the shortest input array is exhausted. + * + * @template T An array of input arrays with potentially different element types + * @param arrays Multiple input arrays to be zipped together + * @returns A generator yielding tuples containing one element from each input array + * + * @example + * const nums = [1, 2, 3]; + * const strs = ['a', 'b', 'c']; + * [...zip(nums, strs)] // [[1, 'a'], [2, 'b'], [3, 'c']] + */ export function* zip( ...arrays: [...{ [K in keyof T]: T[K][] }] ): Generator { + // Calculate the length of the shortest input array const lengths = arrays.map((array) => array.length); const zipLength = Math.min(...lengths); + + // Yield tuples of elements at each index for (let i = 0; i < zipLength; ++i) { yield arrays.map((array) => array[i]) as T; } } +/** + * Zips an object of arrays, creating objects with corresponding elements. + * Stops iterating when the shortest input array is exhausted. + * + * @template T An object containing arrays of potentially different types + * @param arrays Object with array values to be zipped + * @returns A generator yielding objects with one element from each input array + * + * @example + * const data = { + * names: ['Alice', 'Bob'], + * ages: [30, 25], + * cities: ['New York', 'London'] + * }; + * [...structZip(data)] + * // [{ names: 'Alice', ages: 30, cities: 'New York' }, + * // { names: 'Bob', ages: 25, cities: 'London' }] + */ export function* structZip>( arrays: T, ): Generator< @@ -15,8 +49,11 @@ export function* structZip>( void, unknown > { + // Calculate the length of the shortest input array const lengths = Object.values(arrays).map((array) => array.length); const zipLength = Math.min(...lengths); + + // Yield objects with elements at each index for (let i = 0; i < zipLength; ++i) { yield Object.fromEntries( Object.entries(arrays).map(([name, array]) => [name, array[i]]), @@ -24,11 +61,32 @@ export function* structZip>( } } +/** + * Converts an optional value to an array, handling undefined cases. + * + * @template T The type of the input value + * @param value Optional input value + * @returns An array containing the value, or an empty array if undefined + * + * @example + * maybe(5) // [5] + * maybe(undefined) // [] + */ export function maybe(value: T | undefined): T[] { if (value === undefined) return []; return [value]; } +/** + * Generates adjacent pairs from an input array. + * + * @template T The type of elements in the input array + * @param items Input array to generate pairs from + * @returns A generator yielding consecutive pairs of elements + * + * @example + * [...pairwise([1, 2, 3, 4])] // [[1,2], [2,3], [3,4]] + */ export function* pairwise(items: T[]): IterableIterator<[T, T]> { const iterator = items[Symbol.iterator](); let { value: a } = iterator.next(); @@ -41,7 +99,17 @@ export function* pairwise(items: T[]): IterableIterator<[T, T]> { } } -export function* chain(...arrays: T[][]): IterableIterator { +/** + * Chains multiple arrays into a single iterable sequence. + * + * @template T The type of elements in the input arrays + * @param arrays Multiple input arrays to be chained together + * @returns A generator yielding all elements from input arrays in order + * + * @example + * [...chain([1, 2], [3, 4], [5, 6])] // [1, 2, 3, 4, 5, 6] + */ +export function* chain(...arrays: ReadonlyArray): IterableIterator { for (const array of arrays) { yield* array; } diff --git a/typedoc.jsonc b/typedoc.jsonc index a662f08..12d7cca 100644 --- a/typedoc.jsonc +++ b/typedoc.jsonc @@ -6,5 +6,6 @@ "hostedBaseUrl": "https://tmr232.github.io/function-graph-overview/docs", "entryPoints": ["src/control-flow/*"], "readme": "docs/Development.md", - "name": "Function Graph Overview" + "name": "Function Graph Overview", + "sort": ["source-order"] }