Skip to documentation
SLOP

tiny.smg.graph

Reference tiny.smg graph

Defined in tiny.smg.

API (29)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callstiny.smggraph
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callersgraph.SuffixIteratornextNodegraph.SuffixIteratornext
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsgraph.SuffixIteratornextprivate; no linktools.smg.src.graphmatchesDottedSuffixprivate; no linktools.smg.src.graphsuffixEntryMatchesgraph.SuffixIteratornextNode
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate; no linktools.smg.src.graphcheckGraphCloneAllocationFailuresgraphclonetest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: graph clone owns payload indepe...test; no linktools.smg.src.graphtest: node removal compacts storage i...+3 moregraphaddEdgeTrackedgraphaddEdge
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsgraphaddEdgeprivate; no linktools.smg.src.graphcheckGraphAllocationFailuresgraphedgeKeygraphedgeKeyIntographedgeKeyLenmodelmergePairsgraphaddEdgeTracked
Static calls · unresolved targets: 4 · external targets: 4.
Called byCallsprivate sourcelib.choir.src.egraph.patternaddTestBinaryprivate sourcelib.choir.src.egraph.patternaddTestConstanttiny.choiregraph.patterninstantiateprivate; no linktools.smg.src.graphcheckGraphAllocationFailuresprivate; no linktools.smg.src.graphcheckGraphCloneAllocationFailures+7 moreprivate; no linktools.smg.src.graphinvalidateSuffixIndexmodelmergePairsgraphaddNode
Static calls · unresolved targets: 5 · external targets: 4.
Called byCallsNo direct callersgraphsuffixIteratorgraphappendSuffixMatches
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsprivate; no linktools.smg.src.graphcheckGraphAllocationFailurestest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: node removal compacts storage i...test; no linktools.smg.src.graphtest: suffix lookup preserves linear ...SuffixIndexactivateSuffixIndexdeinitSuffixIndexfillSuffixIndexinitprivate; no linktools.smg.src.graphsuffixIndexLimitsgraphbuildSuffixIndex
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate; no linktools.smg.src.graphcheckGraphCloneAllocationFailurestest; no linktools.smg.src.graphtest: graph clone owns payload indepe...graphaddEdgegraphaddNodegraphdeinitgraphinitgraphreserveAdjacency+6 moregraphclone
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callsprivate; no linkfun.sdfii.src.backend.accy.framerunCpuGraphprivate; no linkfun.sdfii.src.backend.accy.lowerexpectKernelMatchesCpuAtPointsprivate; no linkfun.sdfii.src.backend.accy.marchexpectMarchMatchesCputest; no linkfun.sdfii.src.backend.accy.marchtest: node lowering matches the cpu e...test; no linkfun.sdfii.src.backend.accy.testtest: march kernel serves frame rays ...+256 moregraphdeinit
Static calls · unresolved targets: 0 · external targets: 11.
Called byCallsgraphaddEdgeTrackedgraphremoveEdgegraphedgeKeyIntographedgeKeyLengraphedgeKey
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callsgraphaddEdgeTrackedgraphedgeKeyprivate; no linktools.smg.src.graphpruneEdgeIndexgraphremoveEdgegraphedgeKeyInto
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsgraphaddEdgeTrackedgraphedgeKeyprivate; no linktools.smg.src.graphmaxRetiredEdgeKeyLenprivate; no linktools.smg.src.graphpruneEdgeIndexgraphremoveEdgegraphedgeKeyLen
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest; no linktools.smg.src.graphtest: graph clone owns payload indepe...test; no linktools.smg.src.testtest: persisted clean rescan preserve...test; no linktools.smg.src.testtest: persisted ordinary scan retires...graphgetNode
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsNo direct callstest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: graph clone owns payload indepe...test; no linktools.smg.src.graphtest: node removal compacts storage i...graphincomingEdges
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsNo direct callsprivate; no linktools.smg.src.graphcheckGraphAllocationFailuresprivate; no linktools.smg.src.graphcheckGraphCloneAllocationFailuresgraphclonetest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: graph clone owns payload indepe...+4 moregraphinit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callersmodelpairValuegraphisScan
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: graph clone owns payload indepe...test; no linktools.smg.src.graphtest: node removal compacts storage i...test; no linktools.smg.src.testtest: persisted ordinary scan retires...graphoutgoingEdges
Static calls · unresolved targets: 1 · external targets: 0.
Called byCallsprivate; no linktools.smg.src.graphcheckGraphAllocationFailurestest; no linktools.smg.src.graphtest: node removal compacts storage i...private; no linktools.smg.src.graphdropEdgeReferencegraphedgeKeygraphedgeKeyIntographedgeKeyLengraphremoveEdge
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsprivate; no linktools.smg.src.graphcheckGraphAllocationFailurestest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: node removal compacts storage i...test; no linktools.smg.src.graphtest: suffix lookup preserves linear ...private; no linktools.smg.src.graphcompactAdjacencyprivate; no linktools.smg.src.graphcompactEdgesprivate; no linktools.smg.src.graphcompactNodesprivate; no linktools.smg.src.graphinvalidateSuffixIndexprivate; no linktools.smg.src.graphmaxRetiredEdgeKeyLen+4 moregraphremoveNodes
Static calls · unresolved targets: 0 · external targets: 8.
Called byCallsNo direct callsgraphclonegraphreserveAdjacency
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callsgraphclonegraphreserveEdges
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callsgraphclonegraphreserveNodes
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallsNo direct callstest; no linktools.smg.src.graphtest: batch node removal rebuilds gra...test; no linktools.smg.src.graphtest: suffix lookup preserves linear ...graphsuffixIndexReady
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsgraphappendSuffixMatchesprivate; no linktools.smg.src.graphsuffixMatchCounttest; no linktools.smg.src.graphtest: suffix lookup preserves linear ...graphsuffixIterator
Static calls · unresolved targets: 0 · external targets: 1.

Source: tools/smg/src/graph.zig

zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const model = @import("model.zig");pub const edge_key_separator: u8 = 0x1f;pub const Error = error{    NodeNotFound,    EdgeNotFound,    AmbiguousName,    CapacityOverflow,};const SuffixEntry = struct {    hash: u64,    node: u32,    start: u32,};pub const SuffixIndex = struct {    pub const Limits = struct {        entries: usize,        nodes: usize,        max_name_bytes: usize,    };    pub const Capacity = struct {        entries: usize,        nodes: usize,        max_name_bytes: usize,        bytes: usize,        pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {            if (limits.nodes > std.math.maxInt(u32)) return error.CapacityOverflow;            if (limits.max_name_bytes > std.math.maxInt(u32)) return error.CapacityOverflow;            return .{                .entries = limits.entries,                .nodes = limits.nodes,                .max_name_bytes = limits.max_name_bytes,                .bytes = std.math.mul(usize, limits.entries, @sizeOf(SuffixEntry)) catch return error.CapacityOverflow,            };        }    };    pub const InitError = std.mem.Allocator.Error || error{CapacityOverflow};    pub const claim: alloc_phase.capacity.Declaration = .{        .source = .{            .id = "smg.suffix_index",            .kind = .phase_static,            .limit_source = .caller,            .storage = .{                .covered = &.{                    .{                        .id = "one_compact_hash_node_and_suffix_start_record_per_d_8a2d437489ef",                        .lifetime = .steady,                        .detail = "one compact hash, node, and suffix-start record per dotted-name suffix",                    },                },                .excluded = &.{                    "graph nodes and decoded name storage referenced by index records",                    "caller-owned resolution result arrays",                    "linear fallback after graph mutation invalidates the snapshot",                },            },            .capacity = .{                .inputs = &.{                    alloc_phase.capacity.bindInput(Limits, "entries", "entries"),                    alloc_phase.capacity.bindInput(Limits, "max_name_bytes", "max_name_bytes"),                    alloc_phase.capacity.bindInput(Limits, "nodes", "nodes"),                },                .type_selectors = &.{},                .nodes = &.{                    .{ .input = 0 },                    .{ .input = 1 },                    .{ .input = 2 },                    .{ .add = .{ .left = 0, .right = 1 } },                    .{ .add = .{ .left = 3, .right = 2 } },                },                .assertions = &.{.{                    .scope = .closure_total,                    .measure = .retained,                    .relation = .upper_bound,                    .expression = 4,                }},            },            .overload = .{                .kind = .reject_before_seal,                .detail = "checked entry and byte counts plus u32 node and name bounds reject overflow or OOM before activation",            },            .risks = .{                .transitive = .{                    .status = .open,                    .detail = "standard hash and sort helpers are allocation-free in the witness but lack a transitive allocation-closure certificate",                },                .foreign = .{                    .status = .excluded,                    .detail = "the index is process-local caller-owned memory with no operating-system or callback edge",                },            },            .obligations = &.{                .{ .key = "smg_suffix_index_capacity_capacity_model", .role = .capacity_model },                .{ .key = "smg_suffix_index_capacity_overload", .role = .overload },                .{ .key = "smg_suffix_index_oom", .role = .overload },                .{ .key = "smg_suffix_index_sealed_transitive_risk", .role = .transitive_risk },                .{ .key = "smg_suffix_index_sealed_foreign_risk", .role = .foreign_risk },                .{ .key = "smg_suffix_index_invalidation", .role = .custom },            },        },        .bindings = .{            .owner = @This(),            .seal = .{                .family = alloc_phase.capacity.selector(@This().activate),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },            .teardown = .{                .family = alloc_phase.capacity.selector(@This().deinit),                .premise = .{                    .class = .checked_semantic_fact,                    .authority = .checker,                },            },        },    };    phase: alloc_phase.capacity.Phase,    capacity: Capacity,    entries: []SuffixEntry,    pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!SuffixIndex {        const capacity = try Capacity.derive(limits);        const entries = if (capacity.entries == 0)            @constCast((&[_]SuffixEntry{})[0..])        else            try allocator.alloc(SuffixEntry, capacity.entries);        return .{            .phase = .initialization,            .capacity = capacity,            .entries = entries,        };    }    pub fn fill(self: *SuffixIndex, nodes: []const model.Node) void {        std.debug.assert(self.phase == .initialization);        std.debug.assert(nodes.len == self.capacity.nodes);        var filled: usize = 0;        for (nodes, 0..) |node, node_index| {            std.debug.assert(node.name.len <= self.capacity.max_name_bytes);            var start: usize = 0;            while (start < node.name.len) {                std.debug.assert(filled < self.entries.len);                self.entries[filled] = .{                    .hash = std.hash_map.hashString(node.name[start..]),                    .node = @intCast(node_index),                    .start = @intCast(start),                };                filled += 1;                const dot = std.mem.indexOfScalar(u8, node.name[start..], '.') orelse break;                start += dot + 1;            }        }        std.debug.assert(filled == self.entries.len);        std.mem.sort(SuffixEntry, self.entries, {}, suffixEntryLess);    }    pub fn activate(self: *SuffixIndex) void {        std.debug.assert(self.phase == .initialization);        self.phase = .steady;    }    pub fn deinit(self: *SuffixIndex, allocator: std.mem.Allocator) void {        std.debug.assert(self.phase != .teardown);        self.phase = .teardown;        if (self.entries.len != 0) allocator.free(self.entries);        self.* = undefined;    }    pub fn hashMatches(self: *const SuffixIndex, hash: u64) []const SuffixEntry {        std.debug.assert(self.phase == .steady);        const start = suffixHashLowerBound(self.entries, hash);        const end = suffixHashUpperBound(self.entries[start..], hash) + start;        return self.entries[start..end];    }};comptime {    alloc_phase.capacity.requireAllocatorExactOwnerShape(SuffixIndex);}pub const Graph = struct {    allocator: std.mem.Allocator,    nodes: std.ArrayList(model.Node) = .empty,    edges: std.ArrayList(model.Edge) = .empty,    node_index: std.StringHashMap(usize),    suffix_index: ?SuffixIndex = null,    edge_index: std.StringHashMap(usize),    incoming_index: std.ArrayList(std.ArrayList(usize)) = .empty,    outgoing_index: std.ArrayList(std.ArrayList(usize)) = .empty,};pub fn init(allocator: std.mem.Allocator) Graph {    return .{        .allocator = allocator,        .node_index = std.StringHashMap(usize).init(allocator),        .edge_index = std.StringHashMap(usize).init(allocator),    };}pub fn clone(allocator: std.mem.Allocator, scratch: std.mem.Allocator, source: Graph) !Graph {    var graph = init(allocator);    errdefer {        for (graph.nodes.items) |*node| model.deinitNode(node, allocator);        for (graph.edges.items) |*edge| model.deinitEdge(edge, allocator);        deinit(&graph);    }    try reserveNodes(&graph, source.nodes.items.len);    for (source.nodes.items) |node| {        var cloned = try model.cloneNode(allocator, node);        errdefer model.deinitNode(&cloned, allocator);        try addNode(&graph, cloned);    }    try reserveEdges(&graph, source.edges.items.len);    const incoming = try scratch.alloc(usize, source.nodes.items.len);    defer scratch.free(incoming);    const outgoing = try scratch.alloc(usize, source.nodes.items.len);    defer scratch.free(outgoing);    for (source.nodes.items, 0..) |_, index| {        incoming[index] = source.incoming_index.items[index].items.len;        outgoing[index] = source.outgoing_index.items[index].items.len;    }    try reserveAdjacency(&graph, incoming, outgoing);    for (source.edges.items) |edge| {        var cloned = try model.cloneEdge(allocator, edge);        errdefer model.deinitEdge(&cloned, allocator);        try addEdge(&graph, cloned);    }    return graph;}pub fn deinit(graph: *Graph) void {    graph.nodes.deinit(graph.allocator);    graph.edges.deinit(graph.allocator);    graph.node_index.deinit();    if (graph.suffix_index) |*index| index.deinit(graph.allocator);    var edge_it = graph.edge_index.iterator();    while (edge_it.next()) |entry| graph.allocator.free(entry.key_ptr.*);    graph.edge_index.deinit();    for (graph.incoming_index.items) |*list| list.deinit(graph.allocator);    graph.incoming_index.deinit(graph.allocator);    for (graph.outgoing_index.items) |*list| list.deinit(graph.allocator);    graph.outgoing_index.deinit(graph.allocator);    graph.* = undefined;}pub fn incomingEdges(graph: *const Graph, name: []const u8) []const usize {    const index = graph.node_index.get(name) orelse return &.{};    return graph.incoming_index.items[index].items;}pub fn outgoingEdges(graph: *const Graph, name: []const u8) []const usize {    const index = graph.node_index.get(name) orelse return &.{};    return graph.outgoing_index.items[index].items;}pub fn reserveNodes(graph: *Graph, count: usize) !void {    const map_count = std.math.cast(u32, count) orelse return Error.CapacityOverflow;    try graph.nodes.ensureTotalCapacityPrecise(graph.allocator, count);    try graph.node_index.ensureTotalCapacity(map_count);    try graph.incoming_index.ensureTotalCapacityPrecise(graph.allocator, count);    try graph.outgoing_index.ensureTotalCapacityPrecise(graph.allocator, count);}pub fn reserveEdges(graph: *Graph, count: usize) !void {    const map_count = std.math.cast(u32, count) orelse return Error.CapacityOverflow;    try graph.edges.ensureTotalCapacityPrecise(graph.allocator, count);    try graph.edge_index.ensureTotalCapacity(map_count);}pub fn reserveAdjacency(graph: *Graph, incoming: []const usize, outgoing: []const usize) !void {    std.debug.assert(incoming.len == graph.incoming_index.items.len);    std.debug.assert(outgoing.len == graph.outgoing_index.items.len);    for (graph.incoming_index.items, incoming) |*list, count| {        try list.ensureTotalCapacityPrecise(graph.allocator, count);    }    for (graph.outgoing_index.items, outgoing) |*list, count| {        try list.ensureTotalCapacityPrecise(graph.allocator, count);    }}pub fn addNode(graph: *Graph, node: model.Node) !void {    if (graph.node_index.get(node.name)) |index| {        var existing = &graph.nodes.items[index];        existing.type = node.type;        if (node.file != null) existing.file = node.file;        if (node.line != null) existing.line = node.line;        if (node.end_line != null) existing.end_line = node.end_line;        if (node.docstring != null) existing.docstring = node.docstring;        if (node.metadata.len != 0) existing.metadata = try model.mergePairs(graph.allocator, existing.metadata, node.metadata);        return;    }    try graph.nodes.append(graph.allocator, node);    errdefer _ = graph.nodes.pop();    try graph.node_index.put(node.name, graph.nodes.items.len - 1);    errdefer _ = graph.node_index.remove(node.name);    try graph.incoming_index.append(graph.allocator, .empty);    errdefer _ = graph.incoming_index.pop();    try graph.outgoing_index.append(graph.allocator, .empty);    errdefer _ = graph.outgoing_index.pop();    invalidateSuffixIndex(graph);}pub fn buildSuffixIndex(graph: *Graph) !void {    if (graph.suffix_index != null) return;    const limits = try suffixIndexLimits(graph.nodes.items);    var index = try SuffixIndex.init(graph.allocator, limits);    errdefer index.deinit(graph.allocator);    index.fill(graph.nodes.items);    index.activate();    graph.suffix_index = index;}pub fn suffixIndexReady(graph: Graph) bool {    return graph.suffix_index != null;}pub const SuffixIterator = struct {    graph: *const Graph,    raw: []const u8,    entries: ?[]const SuffixEntry,    position: usize = 0,    pub fn nextNode(self: *SuffixIterator) ?*const model.Node {        if (self.entries) |entries| {            while (self.position < entries.len) {                const entry = entries[self.position];                self.position += 1;                if (!suffixEntryMatches(self.graph.nodes.items, entry, self.raw)) continue;                return &self.graph.nodes.items[entry.node];            }            return null;        }        while (self.position < self.graph.nodes.items.len) {            const node = &self.graph.nodes.items[self.position];            self.position += 1;            if (matchesDottedSuffix(node.name, self.raw)) return node;        }        return null;    }    pub fn next(self: *SuffixIterator) ?[]const u8 {        const node = self.nextNode() orelse return null;        return node.name;    }};pub fn suffixIterator(graph: *const Graph, raw: []const u8) SuffixIterator {    return .{        .graph = graph,        .raw = raw,        .entries = if (graph.suffix_index) |*index|            index.hashMatches(std.hash_map.hashString(raw))        else            null,    };}pub fn appendSuffixMatches(graph: Graph, allocator: std.mem.Allocator, raw: []const u8, out: *std.ArrayList([]const u8)) !void {    var iterator = suffixIterator(&graph, raw);    while (iterator.next()) |name| try out.append(allocator, name);}fn suffixEntryMatches(nodes: []const model.Node, entry: SuffixEntry, raw: []const u8) bool {    const node = nodes[entry.node];    return std.mem.eql(u8, node.name[entry.start..], raw);}fn matchesDottedSuffix(name: []const u8, suffix: []const u8) bool {    if (!std.mem.endsWith(u8, name, suffix)) return false;    if (name.len == suffix.len) return true;    return name[name.len - suffix.len - 1] == '.';}fn suffixMatchCount(graph: Graph, raw: []const u8) usize {    var count: usize = 0;    var iterator = suffixIterator(&graph, raw);    while (iterator.next() != null) count += 1;    return count;}pub fn addEdge(graph: *Graph, edge: model.Edge) !void {    _ = try addEdgeTracked(graph, edge);}pub fn addEdgeTracked(graph: *Graph, edge: model.Edge) !bool {    if (!graph.node_index.contains(edge.source) or !graph.node_index.contains(edge.target)) return Error.NodeNotFound;    var stack: [4096]u8 = undefined;    const key_len = edgeKeyLen(edge.source, edge.rel, edge.target);    const lookup_key = if (key_len <= stack.len)        edgeKeyInto(stack[0..key_len], edge.source, edge.rel, edge.target)    else        try edgeKey(graph.allocator, edge.source, edge.rel, edge.target);    var lookup_key_owned = key_len > stack.len;    errdefer if (lookup_key_owned) graph.allocator.free(lookup_key);    if (graph.edge_index.get(lookup_key)) |index| {        defer if (lookup_key_owned) graph.allocator.free(lookup_key);        if (edge.metadata.len != 0) graph.edges.items[index].metadata = try model.mergePairs(graph.allocator, graph.edges.items[index].metadata, edge.metadata);        return false;    }    const key = if (lookup_key_owned) key: {        lookup_key_owned = false;        break :key lookup_key;    } else try graph.allocator.dupe(u8, lookup_key);    var key_owned = true;    errdefer if (key_owned) graph.allocator.free(key);    try graph.edges.append(graph.allocator, edge);    errdefer _ = graph.edges.pop();    const edge_position = graph.edges.items.len - 1;    const source_index = graph.node_index.get(edge.source).?;    const target_index = graph.node_index.get(edge.target).?;    try graph.outgoing_index.items[source_index].append(graph.allocator, edge_position);    errdefer _ = graph.outgoing_index.items[source_index].pop();    try graph.incoming_index.items[target_index].append(graph.allocator, edge_position);    errdefer _ = graph.incoming_index.items[target_index].pop();    try graph.edge_index.put(key, edge_position);    key_owned = false;    return true;}pub fn getNode(graph: *const Graph, name: []const u8) ?model.Node {    const index = graph.node_index.get(name) orelse return null;    return graph.nodes.items[index];}const dead_position = std.math.maxInt(usize);pub fn removeNodes(graph: *Graph, names: []const []const u8) !void {    if (names.len == 0) return;    var removed = std.StringHashMap(void).init(graph.allocator);    defer removed.deinit();    try removed.ensureTotalCapacity(@intCast(names.len));    for (names) |name| {        if (!graph.node_index.contains(name)) return Error.NodeNotFound;        removed.putAssumeCapacity(name, {});    }    const node_map = try graph.allocator.alloc(usize, graph.nodes.items.len);    defer graph.allocator.free(node_map);    const edge_map = try graph.allocator.alloc(usize, graph.edges.items.len);    defer graph.allocator.free(edge_map);    const key_buffer = try graph.allocator.alloc(u8, maxRetiredEdgeKeyLen(graph.*, removed));    defer graph.allocator.free(key_buffer);    const kept_nodes = planNodeRemoval(graph.*, removed, node_map);    const kept_edges = planEdgeRemoval(graph.*, removed, edge_map);    std.debug.assert(kept_nodes < graph.nodes.items.len);    invalidateSuffixIndex(graph);    pruneEdgeIndex(graph, edge_map, key_buffer);    pruneNodeIndex(graph, names, node_map);    compactAdjacency(graph, node_map, edge_map, kept_nodes);    compactEdges(graph, edge_map, kept_edges);    compactNodes(graph, node_map, kept_nodes);    std.debug.assert(graph.nodes.items.len == kept_nodes);    std.debug.assert(graph.edges.items.len == kept_edges);    std.debug.assert(graph.node_index.count() == kept_nodes);    std.debug.assert(graph.edge_index.count() == kept_edges);}pub fn removeEdge(graph: *Graph, source: []const u8, rel: []const u8, target: []const u8) !void {    var stack: [4096]u8 = undefined;    const key_len = edgeKeyLen(source, rel, target);    const lookup_key = if (key_len <= stack.len)        edgeKeyInto(stack[0..key_len], source, rel, target)    else        try edgeKey(graph.allocator, source, rel, target);    defer if (key_len > stack.len) graph.allocator.free(lookup_key);    const entry = graph.edge_index.fetchRemove(lookup_key) orelse return Error.EdgeNotFound;    graph.allocator.free(entry.key);    const position = entry.value;    std.debug.assert(position < graph.edges.items.len);    _ = graph.edges.orderedRemove(position);    var index_it = graph.edge_index.valueIterator();    while (index_it.next()) |value| {        std.debug.assert(value.* != position);        if (value.* > position) value.* -= 1;    }    for (graph.incoming_index.items) |*list| dropEdgeReference(list, position);    for (graph.outgoing_index.items) |*list| dropEdgeReference(list, position);}fn dropEdgeReference(list: *std.ArrayList(usize), position: usize) void {    var write: usize = 0;    for (list.items) |reference| {        if (reference == position) continue;        list.items[write] = if (reference > position) reference - 1 else reference;        write += 1;    }    list.shrinkRetainingCapacity(write);}fn maxRetiredEdgeKeyLen(graph: Graph, removed: std.StringHashMap(void)) usize {    var max_len: usize = 0;    for (graph.edges.items) |edge| {        if (!removed.contains(edge.source) and !removed.contains(edge.target)) continue;        max_len = @max(max_len, edgeKeyLen(edge.source, edge.rel, edge.target));    }    return max_len;}fn planNodeRemoval(graph: Graph, removed: std.StringHashMap(void), node_map: []usize) usize {    var kept: usize = 0;    for (graph.nodes.items, 0..) |node, index| {        if (removed.contains(node.name)) {            node_map[index] = dead_position;        } else {            node_map[index] = kept;            kept += 1;        }    }    return kept;}fn planEdgeRemoval(graph: Graph, removed: std.StringHashMap(void), edge_map: []usize) usize {    var kept: usize = 0;    for (graph.edges.items, 0..) |edge, index| {        if (removed.contains(edge.source) or removed.contains(edge.target)) {            edge_map[index] = dead_position;        } else {            edge_map[index] = kept;            kept += 1;        }    }    return kept;}fn pruneEdgeIndex(graph: *Graph, edge_map: []const usize, key_buffer: []u8) void {    for (graph.edges.items, 0..) |edge, index| {        if (edge_map[index] != dead_position) continue;        const key_len = edgeKeyLen(edge.source, edge.rel, edge.target);        std.debug.assert(key_len <= key_buffer.len);        const key = edgeKeyInto(key_buffer[0..key_len], edge.source, edge.rel, edge.target);        const entry = graph.edge_index.fetchRemove(key) orelse unreachable;        graph.allocator.free(entry.key);    }    var values = graph.edge_index.valueIterator();    while (values.next()) |value| {        std.debug.assert(edge_map[value.*] != dead_position);        value.* = edge_map[value.*];    }}fn pruneNodeIndex(graph: *Graph, names: []const []const u8, node_map: []const usize) void {    for (names) |name| _ = graph.node_index.remove(name);    var values = graph.node_index.valueIterator();    while (values.next()) |value| {        std.debug.assert(node_map[value.*] != dead_position);        value.* = node_map[value.*];    }}fn compactAdjacency(graph: *Graph, node_map: []const usize, edge_map: []const usize, kept_nodes: usize) void {    remapAdjacencyLists(graph, graph.incoming_index.items, node_map, edge_map);    remapAdjacencyLists(graph, graph.outgoing_index.items, node_map, edge_map);    graph.incoming_index.shrinkRetainingCapacity(kept_nodes);    graph.outgoing_index.shrinkRetainingCapacity(kept_nodes);}fn remapAdjacencyLists(graph: *Graph, lists: []std.ArrayList(usize), node_map: []const usize, edge_map: []const usize) void {    for (lists, 0..) |*list, index| {        if (node_map[index] == dead_position) {            list.deinit(graph.allocator);            continue;        }        var write: usize = 0;        for (list.items) |reference| {            if (edge_map[reference] == dead_position) continue;            list.items[write] = edge_map[reference];            write += 1;        }        list.shrinkRetainingCapacity(write);        std.debug.assert(node_map[index] <= index);        lists[node_map[index]] = list.*;    }}fn compactEdges(graph: *Graph, edge_map: []const usize, kept_edges: usize) void {    for (graph.edges.items, 0..) |edge, index| {        if (edge_map[index] == dead_position) continue;        std.debug.assert(edge_map[index] <= index);        graph.edges.items[edge_map[index]] = edge;    }    graph.edges.shrinkRetainingCapacity(kept_edges);}fn compactNodes(graph: *Graph, node_map: []const usize, kept_nodes: usize) void {    for (graph.nodes.items, 0..) |node, index| {        if (node_map[index] == dead_position) continue;        graph.nodes.items[node_map[index]] = node;    }    graph.nodes.shrinkRetainingCapacity(kept_nodes);}fn suffixIndexLimits(nodes: []const model.Node) error{CapacityOverflow}!SuffixIndex.Limits {    if (nodes.len > std.math.maxInt(u32)) return error.CapacityOverflow;    var entries: usize = 0;    var max_name_bytes: usize = 0;    for (nodes) |node| {        if (node.name.len > std.math.maxInt(u32)) return error.CapacityOverflow;        max_name_bytes = @max(max_name_bytes, node.name.len);        var suffixes = if (node.name.len == 0)            0        else            std.math.add(usize, std.mem.countScalar(u8, node.name, '.'), 1) catch return error.CapacityOverflow;        if (node.name.len != 0 and node.name[node.name.len - 1] == '.') suffixes -= 1;        entries = std.math.add(usize, entries, suffixes) catch return error.CapacityOverflow;    }    const limits = SuffixIndex.Limits{        .entries = entries,        .nodes = nodes.len,        .max_name_bytes = max_name_bytes,    };    _ = try SuffixIndex.Capacity.derive(limits);    return limits;}fn invalidateSuffixIndex(graph: *Graph) void {    if (graph.suffix_index) |*index| index.deinit(graph.allocator);    graph.suffix_index = null;}fn suffixEntryLess(_: void, left: SuffixEntry, right: SuffixEntry) bool {    return left.hash < right.hash;}fn suffixHashLowerBound(entries: []const SuffixEntry, hash: u64) usize {    var lower: usize = 0;    var upper = entries.len;    while (lower < upper) {        const middle = lower + (upper - lower) / 2;        if (entries[middle].hash < hash) {            lower = middle + 1;        } else {            upper = middle;        }    }    return lower;}fn suffixHashUpperBound(entries: []const SuffixEntry, hash: u64) usize {    var lower: usize = 0;    var upper = entries.len;    while (lower < upper) {        const middle = lower + (upper - lower) / 2;        if (entries[middle].hash <= hash) {            lower = middle + 1;        } else {            upper = middle;        }    }    return lower;}pub fn edgeKey(allocator: std.mem.Allocator, source: []const u8, rel: []const u8, target: []const u8) ![]const u8 {    const out = try allocator.alloc(u8, edgeKeyLen(source, rel, target));    return edgeKeyInto(out, source, rel, target);}pub fn edgeKeyLen(source: []const u8, rel: []const u8, target: []const u8) usize {    return source.len + 1 + rel.len + 1 + target.len;}pub fn edgeKeyInto(out: []u8, source: []const u8, rel: []const u8, target: []const u8) []const u8 {    var index: usize = 0;    @memcpy(out[index..][0..source.len], source);    index += source.len;    out[index] = edge_key_separator;    index += 1;    @memcpy(out[index..][0..rel.len], rel);    index += rel.len;    out[index] = edge_key_separator;    index += 1;    @memcpy(out[index..][0..target.len], target);    return out;}pub fn isScan(pairs: []const model.Pair) bool {    const source = model.pairValue(pairs, "source") orelse return false;    return std.mem.eql(u8, source, "scan");}test "graph clone owns payload independently" {    var source_arena = std.heap.ArenaAllocator.init(std.testing.allocator);    var source_live = true;    defer if (source_live) source_arena.deinit();    const source_allocator = source_arena.allocator();    var source = init(source_allocator);    const metadata = try model.sourcePair(source_allocator, "scan");    const parent = try source_allocator.dupe(u8, "app");    const child = try source_allocator.dupe(u8, "app.main");    try addNode(&source, .{        .name = parent,        .type = try source_allocator.dupe(u8, model.NodeType.module),        .file = try source_allocator.dupe(u8, "app.zig"),        .metadata = metadata,    });    try addNode(&source, .{        .name = child,        .type = try source_allocator.dupe(u8, model.NodeType.function),        .docstring = try source_allocator.dupe(u8, "entrypoint"),        .metadata = metadata,    });    try addEdge(&source, .{        .source = try source_allocator.dupe(u8, parent),        .rel = try source_allocator.dupe(u8, model.RelType.contains),        .target = try source_allocator.dupe(u8, child),        .metadata = metadata,    });    var cloned_arena = std.heap.ArenaAllocator.init(std.testing.allocator);    defer cloned_arena.deinit();    const cloned = try clone(cloned_arena.allocator(), std.testing.allocator, source);    try std.testing.expect(source.nodes.items[0].name.ptr != cloned.nodes.items[0].name.ptr);    try std.testing.expect(source.edges.items[0].source.ptr != cloned.edges.items[0].source.ptr);    source_arena.deinit();    source_live = false;    try std.testing.expectEqualStrings("app.zig", getNode(&cloned, "app").?.file.?);    try std.testing.expectEqualStrings("entrypoint", getNode(&cloned, "app.main").?.docstring.?);    try std.testing.expectEqualStrings("scan", model.pairValue(cloned.edges.items[0].metadata, "source").?);    try std.testing.expectEqual(@as(usize, 1), outgoingEdges(&cloned, "app").len);    try std.testing.expectEqual(@as(usize, 1), incomingEdges(&cloned, "app.main").len);}fn checkGraphCloneAllocationFailures(allocator: std.mem.Allocator) !void {    var source = init(std.testing.allocator);    defer deinit(&source);    try addNode(&source, .{ .name = "app", .type = model.NodeType.module, .file = "app.zig" });    try addNode(&source, .{ .name = "app.main", .type = model.NodeType.function, .docstring = "entrypoint" });    try addEdge(&source, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" });    var cloned = try clone(allocator, allocator, source);    defer {        for (cloned.nodes.items) |*node| model.deinitNode(node, allocator);        for (cloned.edges.items) |*edge| model.deinitEdge(edge, allocator);        deinit(&cloned);    }    try std.testing.expectEqual(@as(usize, 2), cloned.nodes.items.len);    try std.testing.expectEqual(@as(usize, 1), cloned.edges.items.len);}test "graph clone cleans every allocation failure" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkGraphCloneAllocationFailures,        .{},    );}test "graph operations clean up allocation failures" {    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkGraphAllocationFailures,        .{},    );}fn checkGraphAllocationFailures(allocator: std.mem.Allocator) !void {    const source = &(@as([4100]u8, @splat('s')));    const target = &(@as([4100]u8, @splat('t')));    var graph = init(allocator);    defer deinit(&graph);    try buildSuffixIndex(&graph);    try addNode(&graph, .{ .name = source, .type = model.NodeType.module });    try addNode(&graph, .{ .name = target, .type = model.NodeType.function });    removeEdge(&graph, source, model.RelType.calls, target) catch |err| switch (err) {        error.OutOfMemory => return err,        Error.EdgeNotFound => {},        else => return err,    };    try std.testing.expect(try addEdgeTracked(&graph, .{ .source = source, .rel = model.RelType.calls, .target = target }));    try std.testing.expect(!try addEdgeTracked(&graph, .{ .source = source, .rel = model.RelType.calls, .target = target }));    try removeEdge(&graph, source, model.RelType.calls, target);    try removeNodes(&graph, &.{target});}fn modelSuffixIndexBytes(nodes: []const model.Node) ?usize {    if (nodes.len > std.math.maxInt(u32)) return null;    var entries: usize = 0;    for (nodes) |node| {        if (node.name.len > std.math.maxInt(u32)) return null;        var suffixes: usize = 0;        for (node.name) |byte| {            if (byte != '.') continue;            if (suffixes == std.math.maxInt(usize)) return null;            suffixes += 1;        }        if (node.name.len != 0) {            if (suffixes == std.math.maxInt(usize)) return null;            suffixes += 1;        }        if (node.name.len != 0 and node.name[node.name.len - 1] == '.') suffixes -= 1;        if (entries > std.math.maxInt(usize) - suffixes) return null;        entries += suffixes;    }    if (entries > std.math.maxInt(usize) / @sizeOf(SuffixEntry)) return null;    return entries * @sizeOf(SuffixEntry);}test "suffix index capacity matches an independent dotted-name model" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_capacity_capacity_model"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_capacity_overload"),            null,            null,            null,            null,            null,            null,        );    }    const nodes = [_]model.Node{        .{ .name = "", .type = model.NodeType.module },        .{ .name = "app", .type = model.NodeType.module },        .{ .name = "app.main", .type = model.NodeType.function },        .{ .name = "lib.util.main", .type = model.NodeType.function },        .{ .name = "trailing.", .type = model.NodeType.module },    };    const limits = try suffixIndexLimits(&nodes);    const capacity = try SuffixIndex.Capacity.derive(limits);    try std.testing.expectEqual(modelSuffixIndexBytes(&nodes).?, capacity.bytes);    try std.testing.expectEqual(@as(usize, 7), capacity.entries);    const overflow = SuffixIndex.Limits{        .entries = std.math.maxInt(usize),        .nodes = 0,        .max_name_bytes = 0,    };    try std.testing.expectError(error.CapacityOverflow, SuffixIndex.Capacity.derive(overflow));}fn checkSuffixIndexInitAllocationFailures(allocator: std.mem.Allocator) !void {    var index = try SuffixIndex.init(allocator, .{ .entries = 8, .nodes = 3, .max_name_bytes = 32 });    index.deinit(allocator);}test "suffix index initialization cleans allocation failure and retries" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_oom"),            null,            null,            null,            null,            null,            null,        );    }    try std.testing.checkAllAllocationFailures(        std.testing.allocator,        checkSuffixIndexInitAllocationFailures,        .{},    );    const nodes = [_]model.Node{.{ .name = "node", .type = model.NodeType.module }};    var index = try SuffixIndex.init(std.testing.allocator, try suffixIndexLimits(&nodes));    defer index.deinit(std.testing.allocator);    index.fill(&nodes);    index.activate();    try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, index.phase);}test "suffix index fills sorts and resolves while sealed" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_sealed_transitive_risk"),            null,            null,            null,            null,            null,            null,        );    }    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_sealed_foreign_risk"),            null,            null,            null,            null,            null,            null,        );    }    const nodes = [_]model.Node{        .{ .name = "app.main", .type = model.NodeType.function },        .{ .name = "lib.main", .type = model.NodeType.function },        .{ .name = "lib.other", .type = model.NodeType.function },    };    var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);    var index = try SuffixIndex.init(        phase_allocator.initializationAllocator(),        try suffixIndexLimits(&nodes),    );    defer {        if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();        if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();        index.deinit(phase_allocator.teardownAllocator());        phase_allocator.deinit();    }    const entries_pointer = index.entries.ptr;    phase_allocator.seal();    index.fill(&nodes);    const main_hash = std.hash_map.hashString("main");    for (index.entries) |*entry| {        if (suffixEntryMatches(&nodes, entry.*, "other")) entry.hash = main_hash;    }    std.mem.sort(SuffixEntry, index.entries, {}, suffixEntryLess);    index.activate();    var main_count: usize = 0;    const candidates = index.hashMatches(main_hash);    for (candidates) |entry| {        if (suffixEntryMatches(&nodes, entry, "main")) main_count += 1;    }    try std.testing.expectEqual(@as(usize, 3), candidates.len);    try std.testing.expectEqual(@as(usize, 2), main_count);    try std.testing.expectEqual(entries_pointer, index.entries.ptr);    try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());}test "suffix lookup preserves linear semantics across snapshot invalidation" {    comptime {        @stardustClaim(            @import("alloc_phase").capacity.witness(SuffixIndex, "smg_suffix_index_invalidation"),            null,            null,            null,            null,            null,            null,        );    }    var graph = init(std.testing.allocator);    defer deinit(&graph);    try addNode(&graph, .{ .name = "app.main", .type = model.NodeType.function });    try addNode(&graph, .{ .name = "lib.util.main", .type = model.NodeType.function });    try std.testing.expect(!suffixIndexReady(graph));    try std.testing.expectEqual(@as(usize, 2), suffixMatchCount(graph, "main"));    try buildSuffixIndex(&graph);    try std.testing.expect(suffixIndexReady(graph));    try std.testing.expectEqual(@as(usize, 2), suffixMatchCount(graph, "main"));    try std.testing.expectEqual(@as(usize, 1), suffixMatchCount(graph, "util.main"));    var indexed = suffixIterator(&graph, "util.main");    const indexed_node = indexed.nextNode().?;    try std.testing.expect(indexed_node == &graph.nodes.items[graph.node_index.get("lib.util.main").?]);    try std.testing.expect(indexed.nextNode() == null);    try addNode(&graph, .{ .name = "pkg.helper", .type = model.NodeType.function });    try std.testing.expect(!suffixIndexReady(graph));    try std.testing.expectEqual(@as(usize, 1), suffixMatchCount(graph, "helper"));    try buildSuffixIndex(&graph);    try std.testing.expectEqual(@as(usize, 1), suffixMatchCount(graph, "helper"));    try removeNodes(&graph, &.{"app.main"});    try std.testing.expect(!suffixIndexReady(graph));    try std.testing.expectEqual(@as(usize, 1), suffixMatchCount(graph, "main"));    try buildSuffixIndex(&graph);    try std.testing.expectEqual(@as(usize, 1), suffixMatchCount(graph, "main"));    var deferred = init(std.testing.allocator);    defer deinit(&deferred);    try addNode(&deferred, .{ .name = "app.main", .type = model.NodeType.function });    try addNode(&deferred, .{ .name = "app", .type = model.NodeType.module });    try addEdge(&deferred, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" });    try removeNodes(&deferred, &.{"app.main"});    try std.testing.expect(!suffixIndexReady(deferred));}test "node removal compacts storage in place" {    var graph = init(std.testing.allocator);    defer deinit(&graph);    for ([_][]const u8{ "app", "app.first", "app.second", "keep" }) |name| {        try addNode(&graph, .{ .name = name, .type = model.NodeType.function });    }    try addEdge(&graph, .{ .source = "app", .rel = model.RelType.contains, .target = "app.first" });    try addEdge(&graph, .{ .source = "keep", .rel = model.RelType.calls, .target = "app.second" });    try addEdge(&graph, .{ .source = "app", .rel = model.RelType.calls, .target = "keep" });    try buildSuffixIndex(&graph);    const nodes_ptr = graph.nodes.items.ptr;    const edges_ptr = graph.edges.items.ptr;    const nodes_capacity = graph.nodes.capacity;    const edges_capacity = graph.edges.capacity;    try removeNodes(&graph, &.{ "app.first", "app.second" });    try std.testing.expectEqual(nodes_ptr, graph.nodes.items.ptr);    try std.testing.expectEqual(edges_ptr, graph.edges.items.ptr);    try std.testing.expectEqual(nodes_capacity, graph.nodes.capacity);    try std.testing.expectEqual(edges_capacity, graph.edges.capacity);    try std.testing.expectEqual(@as(usize, 2), graph.nodes.items.len);    try std.testing.expectEqual(@as(usize, 1), graph.edges.items.len);    try std.testing.expectEqual(@as(usize, 1), outgoingEdges(&graph, "app").len);    try std.testing.expectEqual(@as(usize, 1), incomingEdges(&graph, "keep").len);    try std.testing.expectEqualStrings("keep", graph.edges.items[graph.edge_index.get("app\x1fcalls\x1fkeep").?].target);    try removeEdge(&graph, "app", model.RelType.calls, "keep");    try std.testing.expectEqual(@as(usize, 0), graph.edges.items.len);    try std.testing.expectEqual(@as(usize, 0), outgoingEdges(&graph, "app").len);    try std.testing.expectEqual(@as(usize, 0), incomingEdges(&graph, "keep").len);    try std.testing.expectEqual(edges_ptr, graph.edges.items.ptr);}test "batch node removal rebuilds graph indexes once" {    var graph = init(std.testing.allocator);    defer deinit(&graph);    for ([_][]const u8{ "app", "app.first", "app.second", "keep" }) |name| {        try addNode(&graph, .{ .name = name, .type = model.NodeType.function });    }    try addEdge(&graph, .{ .source = "app", .rel = model.RelType.contains, .target = "app.first" });    try addEdge(&graph, .{ .source = "app", .rel = model.RelType.contains, .target = "app.second" });    try addEdge(&graph, .{ .source = "keep", .rel = model.RelType.calls, .target = "app.first" });    try addEdge(&graph, .{ .source = "app", .rel = model.RelType.calls, .target = "keep" });    try buildSuffixIndex(&graph);    try removeNodes(&graph, &.{ "app.first", "app.second" });    try std.testing.expectEqual(@as(usize, 2), graph.nodes.items.len);    try std.testing.expectEqual(@as(usize, 1), graph.edges.items.len);    try std.testing.expect(graph.node_index.contains("app"));    try std.testing.expect(graph.node_index.contains("keep"));    try std.testing.expect(!graph.node_index.contains("app.first"));    try std.testing.expect(!suffixIndexReady(graph));    try buildSuffixIndex(&graph);    try std.testing.expectEqual(@as(usize, 0), suffixMatchCount(graph, "first"));    try std.testing.expectEqual(@as(usize, 0), incomingEdges(&graph, "app").len);    try std.testing.expectEqual(@as(usize, 1), incomingEdges(&graph, "keep").len);    try std.testing.expectEqual(@as(usize, 1), outgoingEdges(&graph, "app").len);    try std.testing.expectEqual(@as(usize, 0), outgoingEdges(&graph, "keep").len);}

Source: tools/smg/src/root.zig:23

zig
pub const graph = @import("graph.zig");

Complete caller list for graph.addEdge

8 direct callers.

Complete caller list for graph.addNode

12 direct callers.

Complete call list for graph.clone

11 direct calls.

Complete caller list for graph.deinit

261 direct callers.

Complete caller list for graph.init

9 direct callers.

Complete call list for graph.removeNodes

9 direct calls.

Audit

Definitions28
Public names28
Members8
Version26.7.0
Revisiondaab053ee433