From b3990d915f5c82df6e5335b86ccbef58f161eeea Mon Sep 17 00:00:00 2001 From: zjp Date: Wed, 28 Jan 2026 14:53:11 +0000 Subject: [PATCH 01/16] ui(topbar): remove view selection --- ui/app/components/widget/TopBar.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/components/widget/TopBar.vue b/ui/app/components/widget/TopBar.vue index 85cadb0..7853858 100644 --- a/ui/app/components/widget/TopBar.vue +++ b/ui/app/components/widget/TopBar.vue @@ -19,7 +19,7 @@ - + diff --git a/ui/app/components/widget/SearchFn.vue b/ui/app/components/widget/SearchFn.vue index 5e70ce7..df7a918 100644 --- a/ui/app/components/widget/SearchFn.vue +++ b/ui/app/components/widget/SearchFn.vue @@ -16,6 +16,7 @@
+
@@ -36,10 +37,12 @@ const sort = (a: SearchFnItem, b: SearchFnItem) => a.name.localeCompare(b.name) const fullFns = computed(() => { const original = props.v_fn + const filterUnsafe = search.value.unsafeOnly const filterTags = search.value.withTags const filterText = search.value.text ? search.value.text.toLowerCase() : "" - return (filterTags || filterText) ? original.filter(i => { + return (filterUnsafe || filterTags || filterText) ? original.filter(i => { let ret = true + if (filterUnsafe) ret &&= isUnsafe(i.name) if (filterTags) ret &&= i.tags.length !== 0 if (filterText) ret &&= ( i.name.toLowerCase().includes(filterText) diff --git a/ui/app/components/widget/TopBar.vue b/ui/app/components/widget/TopBar.vue index 80d238d..ccbd91c 100644 --- a/ui/app/components/widget/TopBar.vue +++ b/ui/app/components/widget/TopBar.vue @@ -116,7 +116,7 @@ const share = defineModel("share", { required: true }) const shareHandle = () => { share.value = true } // Search fn -const search = ref({ withTags: true, text: "", page: 1, itemsPerPage: 20 }) +const search = ref({ withTags: true, unsafeOnly: true, text: "", page: 1, itemsPerPage: 20 }) const searchFnItems = computed(() => { return Object.keys(navi.value.name_to_id).map(name => { return { name, tags: getTag(name, props.tags, true) } diff --git a/ui/app/lib/topbar.ts b/ui/app/lib/topbar.ts index 1b1f401..795f9a3 100644 --- a/ui/app/lib/topbar.ts +++ b/ui/app/lib/topbar.ts @@ -195,7 +195,7 @@ export type BarPlotData = { value: number, } -export type Search = { withTags: boolean, text: string, page: number, itemsPerPage: number } +export type Search = { withTags: boolean, unsafeOnly: boolean, text: string, page: number, itemsPerPage: number } export type SearchFnItem = { name: string, tags: string[] } From facdbf040913421dad9c2c8d4123292ec7271ed4 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 15:16:11 +0000 Subject: [PATCH 10/16] ui(graph): enlarge spacing between field and caller nodes --- ui/app/utils/graph.ts | 62 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index b40015c..4eac475 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -1,4 +1,4 @@ -import { Position, type Edge, type Node } from "@vue-flow/core"; +import { MarkerType, Position, type Edge, type Node } from "@vue-flow/core"; import type { ELK, ElkNode, LayoutOptions } from "elkjs"; import { FieldAccessKind, type Caller, AdtFnKind, type Callees, type CalleeInfo } from "~/lib/output"; import { idAdt, idEdge, idAdtFnKind, idTag, idField, idCalleeWithAdt, idCalleeKindAdt, idCalleeNonGeneric } from "~/lib/graph"; @@ -195,6 +195,10 @@ export class PlotConfig { edgeType(): string { return this.flowOpts.edge as string } + + nodeKind(id: string) { + return this.id_to_item[id]!.kind + } } export enum NodeKind { @@ -542,7 +546,10 @@ export class Plot { ...dimField }) // | field (source) | -> | caller (target) | - edges.push({ id: idEdge(root.id, fieldID,), source: fieldID, target: root.id, type: edgeType }) + edges.push({ + id: idEdge(root.id, fieldID,), source: fieldID, target: root.id, type: edgeType, + label: field.access as string, markerStart: { type: MarkerType.Arrow, color: 'gray', width: 25, height: 25 } + }) } children.push(root) @@ -567,22 +574,22 @@ export class Plot { const tree = await this.elk.layout(graph); const nodes: Node[] = []; - for (const adtOrCaller of tree.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(adtOrCaller)); + for (const adtOrCallerOrCallee of tree.children ?? []) { + nodes.push(config.elkNode_to_vuewFlowNode(adtOrCallerOrCallee)); - const isAdt = id_to_item[adtOrCaller.id]?.kind === NodeKind.Adt + const isAdt = config.nodeKind(adtOrCallerOrCallee.id) === NodeKind.Adt if (isAdt) { - for (const fieldOrCaller of adtOrCaller.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(fieldOrCaller, { parentNode: adtOrCaller.id })) + for (const fieldOrCaller of adtOrCallerOrCallee.children ?? []) { + nodes.push(config.elkNode_to_vuewFlowNode(fieldOrCaller, { parentNode: adtOrCallerOrCallee.id })) for (const tag of fieldOrCaller.children ?? []) { nodes.push(config.elkNode_to_vuewFlowNode(tag, { parentNode: fieldOrCaller.id })) } } } else { - const caller = adtOrCaller - for (const tag of caller.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(tag, { parentNode: caller.id })) + const func = adtOrCallerOrCallee + for (const tag of func.children ?? []) { + nodes.push(config.elkNode_to_vuewFlowNode(tag, { parentNode: func.id })) } } } @@ -618,18 +625,47 @@ export class Plot { // Don't overlap large adt node with callees. if (adt) { - const adtNode = refinedNodes.find(n => id_to_item[n.id]!.kind === NodeKind.Adt) - if (adtNode) { + const fieldNodes = [] + let adtNode: Node | null = null + let callerNode: Node | null = null + + for (const node of nodes) { + switch (config.nodeKind(node.id)) { + case NodeKind.Field: { fieldNodes.push(node); break }; + case NodeKind.Adt: { adtNode = node; break }; + case NodeKind.UnsafeRoot: case NodeKind.SafeRoot: { callerNode = node; break }; + } + } + + if (adtNode !== null) { const adtMaxX = (typeof adtNode.width === "number") ? (adtNode.position.x + adtNode.width) : null if (adtMaxX) { for (const node of refinedNodes) { - const kind = id_to_item[node.id]!.kind + const kind = config.nodeKind(node.id) if (kind === NodeKind.UnsafeFn || kind === NodeKind.SafeFn) { const x = node.position.x if (x < adtMaxX) node.position.x = adtMaxX + 50 } } } + + // Enlarge fields and caller spacing. + let enlarge_spacing = false + const spacing = config.px * 7 // in the unit of a mono char with + for (const field of fieldNodes) { + if (enlarge_spacing) break + const gap = callerNode!.position.x - field.position.x - (field.width as number) + if (gap < spacing) enlarge_spacing = true + } + + if (enlarge_spacing) { + // Move adt node left. All elements inside will also be moved left. + adtNode.position.x -= spacing; + // Widen adt node for enclosing all elements . + adtNode.width = adtNode.width as number + spacing + // Move caller node right. + callerNode!.position.x += spacing + } } } From 590d0f503e402600b2f59eba39e61347f6254b55 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 15:51:03 +0000 Subject: [PATCH 11/16] ui(graph): add field and caller header --- ui/app/utils/graph.ts | 58 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index 4eac475..6a6824f 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -142,7 +142,7 @@ export class PlotConfig { return nodes } - elkNode_to_vuewFlowNode(node: ElkNode, opts: any = {}): Node { + elkNode_to_vueFlowNode(node: ElkNode, opts: any = {}): Node { const id = node.id const kind = this.id_to_item[id]!.kind @@ -162,6 +162,29 @@ export class PlotConfig { } } + fieldHeaderNode(x: number, y: number, adt: string, nFields: number, parent: string): Node { + const id = `FieldHeader@${adt}` + this.id_to_item[id] = { name: adt, kind: NodeKind.FieldHeader } + const label = (nFields === 1) ? "Field" : "Fields" + const dim = this.size(label) + return { + id, label, width: dim.width, height: dim.height, parentNode: parent, + position: { x, y }, class: this.nodeClass(id), type: "no-handle", + } + } + + callerHeaderNode(x: number, y: number, caller: string, parent: string): Node { + const id = `CallerHeader@${caller}` + this.id_to_item[id] = { name: caller, kind: NodeKind.CallerHeader } + const label = "Caller" + const dim = this.size(label) + return { + id, label, width: dim.width, height: dim.height, parentNode: parent, + position: { x, y }, class: this.nodeClass(id), type: "no-handle", + } + } + + nodeClass(id: string) { let ret = nodeKindClass(this.id_to_item[id]!.kind) if (this.adt_border_b_0.has(id)) { @@ -213,6 +236,7 @@ export enum NodeKind { FnKind, Field, FieldHeader, + CallerHeader, } function nodeKindClass(kind: NodeKind) { @@ -223,7 +247,7 @@ function nodeKindClass(kind: NodeKind) { case NodeKind.Adt: return "upg-node-adt"; case NodeKind.FnKind: return "upg-node-adt-fn-kind"; case NodeKind.Field: return "upg-node-fn"; - case NodeKind.FieldHeader: return "upg-node-adt-fn-kind"; + case NodeKind.FieldHeader: case NodeKind.CallerHeader: return "upg-node-adt-fn-kind"; default: return ""; } } @@ -575,21 +599,21 @@ export class Plot { const nodes: Node[] = []; for (const adtOrCallerOrCallee of tree.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(adtOrCallerOrCallee)); + nodes.push(config.elkNode_to_vueFlowNode(adtOrCallerOrCallee)); const isAdt = config.nodeKind(adtOrCallerOrCallee.id) === NodeKind.Adt if (isAdt) { for (const fieldOrCaller of adtOrCallerOrCallee.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(fieldOrCaller, { parentNode: adtOrCallerOrCallee.id })) + nodes.push(config.elkNode_to_vueFlowNode(fieldOrCaller, { parentNode: adtOrCallerOrCallee.id })) for (const tag of fieldOrCaller.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(tag, { parentNode: fieldOrCaller.id })) + nodes.push(config.elkNode_to_vueFlowNode(tag, { parentNode: fieldOrCaller.id })) } } } else { const func = adtOrCallerOrCallee for (const tag of func.children ?? []) { - nodes.push(config.elkNode_to_vuewFlowNode(tag, { parentNode: func.id })) + nodes.push(config.elkNode_to_vueFlowNode(tag, { parentNode: func.id })) } } } @@ -651,11 +675,17 @@ export class Plot { // Enlarge fields and caller spacing. let enlarge_spacing = false + let firstFieldY: number | null = null + let firstFieldX: number | null = null const spacing = config.px * 7 // in the unit of a mono char with for (const field of fieldNodes) { - if (enlarge_spacing) break - const gap = callerNode!.position.x - field.position.x - (field.width as number) + const x = field.position.x + const gap = callerNode!.position.x - x - (field.width as number) if (gap < spacing) enlarge_spacing = true + + const y = field.position.y + firstFieldY = firstFieldY ? Math.min(y, firstFieldY) : y + firstFieldX = firstFieldX ? Math.min(x, firstFieldX) : x } if (enlarge_spacing) { @@ -666,6 +696,18 @@ export class Plot { // Move caller node right. callerNode!.position.x += spacing } + + // Add field and caller header + if (firstFieldY !== null && firstFieldX !== null) { + const parent = adtNode!.id + const y = firstFieldY - config.px * 3 + const fieldHeader = config.fieldHeaderNode(firstFieldX, y, adt.name, fieldNodes.length, parent) + nodes.push(fieldHeader) + + const callerHeaderX = (callerNode!.position.x + (callerNode!.width as number)) / 2 + const callerHeader = config.callerHeaderNode(callerHeaderX, y, fn.name, parent) + nodes.push(callerHeader) + } } } From d5df3abe652b4d57c64760ba4c34d6631f2c0672 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 16:04:56 +0000 Subject: [PATCH 12/16] ui(graph): enlarge node width to label width --- ui/app/utils/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index 6a6824f..1c913a2 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -154,8 +154,9 @@ export class PlotConfig { case NodeKind.UnsafeRoot: case NodeKind.SafeRoot: { type = "default"; break }; } + const labelWidth = node.labels![0]!.width! return { - id, label: node.labels![0]!.text!, width: node.width, height: node.height, + id, label: node.labels![0]!.text!, width: Math.max(node.width!, labelWidth), height: node.height, position: { x: node.x!, y: node.y! }, class: this.nodeClass(id), type, sourcePosition: Position.Right, targetPosition: Position.Left, ...opts From de46124e0c5cfd03e816cab7418f086c53ce3f1a Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 16:18:04 +0000 Subject: [PATCH 13/16] ui(graph): don't overlap caller and callee --- ui/app/utils/graph.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index 1c913a2..6de6b6a 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -648,8 +648,8 @@ export class Plot { updateNodePosition(refinedNodes, refinedEdges); - // Don't overlap large adt node with callees. if (adt) { + // Don't overlap large adt node with callees. const fieldNodes = [] let adtNode: Node | null = null let callerNode: Node | null = null @@ -710,8 +710,27 @@ export class Plot { nodes.push(callerHeader) } } - } + } else { + let calleeNodes: Node[] = [] + let callerNode: Node | null = null + for (const node of nodes) { + switch (config.nodeKind(node.id)) { + case NodeKind.UnsafeFn: case NodeKind.SafeFn: { calleeNodes.push(node); break }; + case NodeKind.UnsafeRoot: case NodeKind.SafeRoot: { callerNode = node; break }; + } + } + + // Don't overlap caller and callees. + let calleeX: number | null = null + calleeNodes.forEach(c => calleeX = calleeX ? Math.min(c.position.x) : c.position.x) + if (calleeX !== null) { + const gap = calleeX - callerNode!.position.x - (callerNode!.width as number) + const spacing = 100 + // Move left if the gap is narrow. Use abs for negative gap (overlapping). + if (gap < spacing) callerNode!.position.x -= (Math.abs(gap) + spacing) + } + } Object.assign(this, { nodes, edges }); } From 2c11ef27a4fe8f11e92e484970aa9bbafaf0e7d2 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 16:26:18 +0000 Subject: [PATCH 14/16] ui(graph): Align caller with first field --- ui/app/utils/graph.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index 6de6b6a..8677920 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -708,6 +708,9 @@ export class Plot { const callerHeaderX = (callerNode!.position.x + (callerNode!.width as number)) / 2 const callerHeader = config.callerHeaderNode(callerHeaderX, y, fn.name, parent) nodes.push(callerHeader) + + // Align caller with first field. + callerNode!.position.y = firstFieldY } } } else { From 2e0ee18217a4236bd291cd80e3450226247e12d0 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 17:52:08 +0000 Subject: [PATCH 15/16] ui(graph): enlarge adt node if caller bloats NOTE: children elk nodes use relative postion to parent node --- ui/app/utils/graph.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index 8677920..f022f16 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -600,12 +600,29 @@ export class Plot { const nodes: Node[] = []; for (const adtOrCallerOrCallee of tree.children ?? []) { - nodes.push(config.elkNode_to_vueFlowNode(adtOrCallerOrCallee)); + const parent = config.elkNode_to_vueFlowNode(adtOrCallerOrCallee) + nodes.push(parent); const isAdt = config.nodeKind(adtOrCallerOrCallee.id) === NodeKind.Adt if (isAdt) { for (const fieldOrCaller of adtOrCallerOrCallee.children ?? []) { - nodes.push(config.elkNode_to_vueFlowNode(fieldOrCaller, { parentNode: adtOrCallerOrCallee.id })) + const child = config.elkNode_to_vueFlowNode(fieldOrCaller, { parentNode: parent.id }) + nodes.push(child) + + const isCaller = root.id === fieldOrCaller.id + if (isCaller) { + // Enlarge adt node when caller has been enlarged due to label width. + // NOTE: must use vue node instead of elk node because we ignore elk node by using label width. + const adtNode = parent + const caller = child + const gapX = (adtNode.width as number) - caller.position.x - (caller.width as number) + const spacingX = config.px * 3 + if (gapX < spacingX) (adtNode.width as number) += ((gapX < 0) ? (-gapX) : 0) + spacingX + + const gapY = (adtNode.height as number) - caller.position.y - (caller.height as number) + const spacingY = config.px * 2 + if (gapY < spacingY) (adtNode.height as number) += ((gapY < 0) ? (-gapY) : 0) + spacingY + } for (const tag of fieldOrCaller.children ?? []) { nodes.push(config.elkNode_to_vueFlowNode(tag, { parentNode: fieldOrCaller.id })) @@ -706,11 +723,9 @@ export class Plot { nodes.push(fieldHeader) const callerHeaderX = (callerNode!.position.x + (callerNode!.width as number)) / 2 - const callerHeader = config.callerHeaderNode(callerHeaderX, y, fn.name, parent) + const callerY = callerNode!.position.y - config.px * 3 + const callerHeader = config.callerHeaderNode(callerHeaderX, callerY, fn.name, parent) nodes.push(callerHeader) - - // Align caller with first field. - callerNode!.position.y = firstFieldY } } } else { From 2113b96853b4235415fdcf926ed2191d3c09b5f3 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 29 Jan 2026 18:03:38 +0000 Subject: [PATCH 16/16] ui(graph): AdtFnKind as caller header --- ui/app/utils/graph.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts index f022f16..d654fc9 100644 --- a/ui/app/utils/graph.ts +++ b/ui/app/utils/graph.ts @@ -174,10 +174,10 @@ export class PlotConfig { } } - callerHeaderNode(x: number, y: number, caller: string, parent: string): Node { + callerHeaderNode(x: number, y: number, caller: string, kind: AdtFnKind, parent: string): Node { const id = `CallerHeader@${caller}` this.id_to_item[id] = { name: caller, kind: NodeKind.CallerHeader } - const label = "Caller" + const label = kind as string const dim = this.size(label) return { id, label, width: dim.width, height: dim.height, parentNode: parent, @@ -620,7 +620,7 @@ export class Plot { if (gapX < spacingX) (adtNode.width as number) += ((gapX < 0) ? (-gapX) : 0) + spacingX const gapY = (adtNode.height as number) - caller.position.y - (caller.height as number) - const spacingY = config.px * 2 + const spacingY = config.px if (gapY < spacingY) (adtNode.height as number) += ((gapY < 0) ? (-gapY) : 0) + spacingY } @@ -724,7 +724,7 @@ export class Plot { const callerHeaderX = (callerNode!.position.x + (callerNode!.width as number)) / 2 const callerY = callerNode!.position.y - config.px * 3 - const callerHeader = config.callerHeaderNode(callerHeaderX, callerY, fn.name, parent) + const callerHeader = config.callerHeaderNode(callerHeaderX, callerY, fn.name, adt.kind, parent) nodes.push(callerHeader) } }