Skip to main content

GraphUtil

A library of generic, graph-agnostic algorithms. Describe your graph with a few callbacks — no bespoke data structure required — and run any algorithm over any representation (adjacency lists, spatial grids, implicit neighbour functions, …).

Submodules

Module Purpose
BFS Breadth-first traversal & unweighted shortest paths
DFS Depth-first traversal & spanning trees
Dijkstra Weighted shortest paths (non-negative) & cost-ordered traversal
AStar Heuristic-guided weighted shortest path
BellmanFord Weighted shortest paths with negative edges & cycle detection
MST Minimum spanning trees (Prim / Kruskal)
TopologicalSort Linear ordering of a DAG (DFS / Kahn)
Connectivity Reachability, components, cycles, bipartite, SCCs

Shared callbacks

Algorithms are driven by callbacks rather than a fixed structure. See each type page for the full signature and worked examples: GetNeighbors (required by every algorithm), GetWeight (weighted algorithms), and GetHeuristic (A*). Path-finders return a PathResult; MST returns Edge lists.

Directed vs. undirected

Because your callbacks define the edges, most algorithms work on directed and undirected graphs alike. Every function carries a directed and/or undirected tag showing which graph kinds it supports — a function tagged with only one (e.g. TopologicalSort is directed-only, MST is undirected-only) gives wrong or meaningless results on the other kind.

local GraphUtil = require(Packages.GraphUtil)

local adjacency = {
    A = { "B", "C" },
    B = { "A", "D" },
    C = { "A", "D" },
    D = { "B", "C" },
}

local result = GraphUtil.BFS.shortestPath("A", "D", function(v)
    return adjacency[v]
end)
-- result.path == { "A", "B", "D" }

Types

GetNeighborsArray

type GetNeighborsArray = (vertexV) → {V}

A callback that returns the neighbours of vertex as an array. The simpler of the two GetNeighbors forms — return whatever's convenient and let the algorithm collect it.

local adjacencyList = {
    A = { "B", "C" },
    B = { "A", "D" },
    C = { "A", "D" },
    D = { "B", "C" },
}

local function getNeighbors(vertex: string): { string }
    -- Return an empty array (never nil) for vertices with no neighbours,
    -- so the algorithms can index the result safely.
    return adjacencyList[vertex] or {}
end

GetNeighborsIterator

type GetNeighborsIterator = (vertexV) → () → V?

A callback that returns a stateful iterator over the neighbours of vertex. More allocation-efficient than GetNeighborsArray — no intermediate table is built — at the cost of managing the iteration state yourself.

local adjacencyList = {
    A = { "B", "C" },
    B = { "A", "D" },
    C = { "A", "D" },
    D = { "B", "C" },
}

local function getNeighbors(vertex: string): () -> string?
    local neighbours = adjacencyList[vertex] or {}
    local i = 0
    return function(): string?
        i += 1
        return neighbours[i]
    end
end

GetNeighbors

type GetNeighbors = GetNeighborsArray | GetNeighborsIterator

Either form of neighbour callback — GetNeighborsArray or GetNeighborsIterator. Accepted by every algorithm in this library and normalised internally, so you can pick whichever form is most natural for your graph representation.

local function arrayForm(vertex: string): { string }
    return adjacencyList[vertex] or {}
end

local ok = BFS.shortestPath("A", "D", arrayForm) -- both forms work

GetWeight

type GetWeight = (
fromV,
toV
) → number

Returns the non-negative weight of the directed edge from from to to. Required by every weighted algorithm (Dijkstra, AStar, BellmanFord, MST).

local edgeWeights = {
    A = { B = 2, C = 3 },
    B = { A = 2, D = 1 },
    C = { A = 3, D = 5 },
    D = { B = 1, C = 5 },
}

local function getWeight(from: string, to: string): number
    return edgeWeights[from][to]
end

GetHeuristic

type GetHeuristic = (
vertexV,
goalV
) → number

An admissible heuristic: an estimate of the cost from vertex to goal that must never overestimate the true cost. Required by AStar; an inadmissible heuristic makes A* fast but no longer guaranteed optimal.

-- Straight-line distance is admissible for movement costs that are
-- never cheaper than a direct line (e.g. grid movement).
local function heuristic(vertex: { Position: Vector3 }, goal: { Position: Vector3 }): number
    return (vertex.Position - goal.Position).Magnitude
end

ShouldStop

type ShouldStop = (
vertexV,
costnumber
) → boolean

An optional cooperative cancellation predicate. Search functions that accept one call it once per expanded vertex — after the goal check, before that vertex's neighbours are relaxed — and abort the search the moment it returns true. Path-finders then return nil (indistinguishable from an unreachable goal); Dijkstra.distances returns the partial result accumulated so far.

Because these algorithms run synchronously to completion, this predicate is the only way to bound an open-ended search. cost is the accumulated cost of vertex — weighted distance for Dijkstra / AStar, hop-count for BFS — so you can stop on a budget; most callers ignore both arguments and stop on a wall-clock deadline.

-- Give up after half a second of searching.
local deadline = os.clock() + 0.5
local result = AStar.shortestPath(a, b, getNeighbors, getWeight, heuristic, function()
    return os.clock() > deadline
end)

-- Or stop once the search cost exceeds a budget of 500.
local result = Dijkstra.shortestPath(a, b, getNeighbors, getWeight, function(_vertex, cost)
    return cost > 500
end)

KIterator

type KIterator = () → K?

A stateless-to-the-caller iterator that yields one value per call, returning nil once exhausted. This is the single-value form returned by BFS.toIterator and DFS.toIterator; for the (vertex, cost) pair form see KVIterator.

The nil-at-exhaustion convention is what makes these iterators directly usable in a plain for loop.

local iter: Types.KIterator<string> = BFS.toIterator(start, getNeighbors)

for vertex in iter do
    print(vertex)
end

KVIterator

type KVIterator = () → (
K?,
V?
)

A stateless-to-the-caller iterator that yields a pair of values per call, returning (nil, nil) once exhausted. This is the (vertex, cost) form returned by Dijkstra.toIterator; for the single-value form see KIterator.

Write the type arguments without ? — the alias applies the optionality itself, since every slot is nil once the traversal is exhausted: KVIterator<string, number> is () -> (string?, number?).

local iter: Types.KVIterator<string, number> = Dijkstra.toIterator(start, getNeighbors, getWeight)

for vertex, cost in iter do
    print(vertex, cost)
end

PathResult

interface PathResult {
path{V}--

Ordered sequence of vertices from start to goal (both inclusive).

costnumber--

Total accumulated edge-weight along the path. For unweighted algorithms this is the hop-count.

}

The result of a path-finding algorithm — returned by BFS.shortestPath, Dijkstra.shortestPath, AStar.shortestPath, and BellmanFord.shortestPath.

local result = Dijkstra.shortestPath("A", "D", getNeighbors, getWeight)
if result then
    print(result.path) -- { "A", "B", "D" }
    print(result.cost) -- 3
end

Edge

interface Edge {
fromV--

The vertex the edge starts at.

toV--

The vertex the edge ends at.

weightnumber--

The edge's weight.

}

An edge in a spanning or search tree, as produced by MST.kruskal and MST.prim.

local edges = MST.kruskal(vertices, getNeighbors, getWeight)
for _, edge in edges do
    print(`{edge.from} -> {edge.to} ({edge.weight})`)
end

Properties

BFS

GraphUtil.BFS: BFS

Breadth-first search — traversal and unweighted (fewest-hop) shortest paths.

DFS

GraphUtil.DFS: DFS

Depth-first search — traversal, reachability sets, and spanning trees.

Dijkstra

GraphUtil.Dijkstra: Dijkstra

Weighted shortest paths (non-negative edges) and cost-ordered traversal.

AStar

GraphUtil.AStar: AStar

Heuristic-guided weighted shortest path — faster than Dijkstra with a good heuristic.

BellmanFord

GraphUtil.BellmanFord: BellmanFord

Weighted shortest paths that tolerate negative edges, with negative-cycle detection.

MST

GraphUtil.MST: MST

Minimum spanning trees via Prim's and Kruskal's algorithms.

TopologicalSort

GraphUtil.TopologicalSort: TopologicalSort

Linear ordering of a directed acyclic graph via DFS post-order or Kahn's algorithm.

Connectivity

GraphUtil.Connectivity: Connectivity

Reachability, connected components, cycle detection, bipartiteness, and SCCs.

Show raw api
{
    "functions": [
        {
            "name": "eachNeighbor",
            "desc": "Normalises either form of `GetNeighbors<V>` into a plain iterator\n`() -> V?`.  The detection works by calling `getNeighbors(vertex)` once\nand checking whether the result is itself a function (iterator form) or\na table (array form).\n\nThis helper is called once per vertex visit inside every algorithm, so\nit must stay cheap.\n\n```lua\nfor neighbor in Types.eachNeighbor(vertex, getNeighbors) do\n    -- ...\nend\n```",
            "params": [
                {
                    "name": "vertex",
                    "desc": "",
                    "lua_type": "V"
                },
                {
                    "name": "getNeighbors",
                    "desc": "",
                    "lua_type": "GetNeighbors<V>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "() -> V?\n"
                }
            ],
            "function_type": "static",
            "private": true,
            "source": {
                "line": 277,
                "path": "lib/graphutil/src/Types.luau"
            }
        }
    ],
    "properties": [
        {
            "name": "BFS",
            "desc": "Breadth-first search — traversal and unweighted (fewest-hop) shortest paths.",
            "lua_type": "BFS",
            "source": {
                "line": 67,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "DFS",
            "desc": "Depth-first search — traversal, reachability sets, and spanning trees.",
            "lua_type": "DFS",
            "source": {
                "line": 74,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "Dijkstra",
            "desc": "Weighted shortest paths (non-negative edges) and cost-ordered traversal.",
            "lua_type": "Dijkstra",
            "source": {
                "line": 81,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "AStar",
            "desc": "Heuristic-guided weighted shortest path — faster than Dijkstra with a good heuristic.",
            "lua_type": "AStar",
            "source": {
                "line": 88,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "BellmanFord",
            "desc": "Weighted shortest paths that tolerate negative edges, with negative-cycle detection.",
            "lua_type": "BellmanFord",
            "source": {
                "line": 95,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "MST",
            "desc": "Minimum spanning trees via Prim's and Kruskal's algorithms.",
            "lua_type": "MST",
            "source": {
                "line": 102,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "TopologicalSort",
            "desc": "Linear ordering of a directed acyclic graph via DFS post-order or Kahn's algorithm.",
            "lua_type": "TopologicalSort",
            "source": {
                "line": 109,
                "path": "lib/graphutil/src/init.luau"
            }
        },
        {
            "name": "Connectivity",
            "desc": "Reachability, connected components, cycle detection, bipartiteness, and SCCs.",
            "lua_type": "Connectivity",
            "source": {
                "line": 116,
                "path": "lib/graphutil/src/init.luau"
            }
        }
    ],
    "types": [
        {
            "name": "GetNeighborsArray",
            "desc": "A callback that returns the neighbours of `vertex` as an array. The simpler\nof the two [GetNeighbors] forms — return whatever's convenient and let the\nalgorithm collect it.\n\n```lua\nlocal adjacencyList = {\n    A = { \"B\", \"C\" },\n    B = { \"A\", \"D\" },\n    C = { \"A\", \"D\" },\n    D = { \"B\", \"C\" },\n}\n\nlocal function getNeighbors(vertex: string): { string }\n    -- Return an empty array (never nil) for vertices with no neighbours,\n    -- so the algorithms can index the result safely.\n    return adjacencyList[vertex] or {}\nend\n```",
            "lua_type": "(vertex: V) -> { V }",
            "source": {
                "line": 35,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "GetNeighborsIterator",
            "desc": "A callback that returns a stateful iterator over the neighbours of\n`vertex`. More allocation-efficient than [GetNeighborsArray] — no\nintermediate table is built — at the cost of managing the iteration state\nyourself.\n\n```lua\nlocal adjacencyList = {\n    A = { \"B\", \"C\" },\n    B = { \"A\", \"D\" },\n    C = { \"A\", \"D\" },\n    D = { \"B\", \"C\" },\n}\n\nlocal function getNeighbors(vertex: string): () -> string?\n    local neighbours = adjacencyList[vertex] or {}\n    local i = 0\n    return function(): string?\n        i += 1\n        return neighbours[i]\n    end\nend\n```",
            "lua_type": "(vertex: V) -> () -> V?",
            "source": {
                "line": 64,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "GetNeighbors",
            "desc": "Either form of neighbour callback — [GetNeighborsArray] or\n[GetNeighborsIterator]. Accepted by every algorithm in this library and\nnormalised internally, so you can pick whichever form is most natural for\nyour graph representation.\n\n```lua\nlocal function arrayForm(vertex: string): { string }\n    return adjacencyList[vertex] or {}\nend\n\nlocal ok = BFS.shortestPath(\"A\", \"D\", arrayForm) -- both forms work\n```",
            "lua_type": "GetNeighborsArray | GetNeighborsIterator",
            "source": {
                "line": 83,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "GetWeight",
            "desc": "Returns the non-negative weight of the directed edge from `from` to `to`.\nRequired by every weighted algorithm ([Dijkstra](/api/Dijkstra),\n[AStar](/api/AStar), [BellmanFord](/api/BellmanFord), [MST](/api/MST)).\n\n```lua\nlocal edgeWeights = {\n    A = { B = 2, C = 3 },\n    B = { A = 2, D = 1 },\n    C = { A = 3, D = 5 },\n    D = { B = 1, C = 5 },\n}\n\nlocal function getWeight(from: string, to: string): number\n    return edgeWeights[from][to]\nend\n```",
            "lua_type": "(from: V, to: V) -> number",
            "source": {
                "line": 106,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "GetHeuristic",
            "desc": "An admissible heuristic: an estimate of the cost from `vertex` to `goal`\nthat must never overestimate the true cost. Required by\n[AStar](/api/AStar); an inadmissible heuristic makes A* fast but no longer\nguaranteed optimal.\n\n```lua\n-- Straight-line distance is admissible for movement costs that are\n-- never cheaper than a direct line (e.g. grid movement).\nlocal function heuristic(vertex: { Position: Vector3 }, goal: { Position: Vector3 }): number\n    return (vertex.Position - goal.Position).Magnitude\nend\n```",
            "lua_type": "(vertex: V, goal: V) -> number",
            "source": {
                "line": 125,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "ShouldStop",
            "desc": "An optional **cooperative cancellation** predicate. Search functions that\naccept one call it once per expanded vertex — after the goal check, before\nthat vertex's neighbours are relaxed — and abort the search the moment it\nreturns `true`. Path-finders then return `nil` (indistinguishable from an\nunreachable goal); [`Dijkstra.distances`](/api/Dijkstra#distances) returns\nthe partial result accumulated so far.\n\nBecause these algorithms run synchronously to completion, this predicate is\nthe only way to bound an open-ended search. `cost` is the accumulated cost\nof `vertex` — weighted distance for [Dijkstra](/api/Dijkstra) /\n[AStar](/api/AStar), hop-count for [BFS](/api/BFS) — so you can stop on a\nbudget; most callers ignore both arguments and stop on a wall-clock deadline.\n\n```lua\n-- Give up after half a second of searching.\nlocal deadline = os.clock() + 0.5\nlocal result = AStar.shortestPath(a, b, getNeighbors, getWeight, heuristic, function()\n    return os.clock() > deadline\nend)\n\n-- Or stop once the search cost exceeds a budget of 500.\nlocal result = Dijkstra.shortestPath(a, b, getNeighbors, getWeight, function(_vertex, cost)\n    return cost > 500\nend)\n```",
            "lua_type": "(vertex: V, cost: number) -> boolean",
            "source": {
                "line": 157,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "KIterator",
            "desc": "A stateless-to-the-caller iterator that yields one value per call,\nreturning `nil` once exhausted. This is the single-value form returned by\n[`BFS.toIterator`](/api/BFS#toIterator) and\n[`DFS.toIterator`](/api/DFS#toIterator); for the `(vertex, cost)` pair form\nsee [KVIterator].\n\nThe `nil`-at-exhaustion convention is what makes these iterators directly\nusable in a plain `for` loop.\n\n```lua\nlocal iter: Types.KIterator<string> = BFS.toIterator(start, getNeighbors)\n\nfor vertex in iter do\n    print(vertex)\nend\n```",
            "lua_type": "() -> K?",
            "source": {
                "line": 180,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "KVIterator",
            "desc": "A stateless-to-the-caller iterator that yields a pair of values per call,\nreturning `(nil, nil)` once exhausted. This is the `(vertex, cost)` form\nreturned by [`Dijkstra.toIterator`](/api/Dijkstra#toIterator); for the\nsingle-value form see [KIterator].\n\nWrite the type arguments *without* `?` — the alias applies the optionality\nitself, since every slot is `nil` once the traversal is exhausted:\n`KVIterator<string, number>` is `() -> (string?, number?)`.\n\n```lua\nlocal iter: Types.KVIterator<string, number> = Dijkstra.toIterator(start, getNeighbors, getWeight)\n\nfor vertex, cost in iter do\n    print(vertex, cost)\nend\n```",
            "lua_type": "() -> (K?, V?)",
            "source": {
                "line": 203,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "PathResult",
            "desc": "The result of a path-finding algorithm — returned by\n[`BFS.shortestPath`](/api/BFS#shortestPath),\n[`Dijkstra.shortestPath`](/api/Dijkstra#shortestPath),\n[`AStar.shortestPath`](/api/AStar#shortestPath), and\n[`BellmanFord.shortestPath`](/api/BellmanFord#shortestPath).\n\n```lua\nlocal result = Dijkstra.shortestPath(\"A\", \"D\", getNeighbors, getWeight)\nif result then\n    print(result.path) -- { \"A\", \"B\", \"D\" }\n    print(result.cost) -- 3\nend\n```",
            "fields": [
                {
                    "name": "path",
                    "lua_type": "{ V }",
                    "desc": "Ordered sequence of vertices from start to goal (both inclusive)."
                },
                {
                    "name": "cost",
                    "lua_type": "number",
                    "desc": "Total accumulated edge-weight along the path. For unweighted algorithms this is the hop-count."
                }
            ],
            "source": {
                "line": 225,
                "path": "lib/graphutil/src/Types.luau"
            }
        },
        {
            "name": "Edge",
            "desc": "An edge in a spanning or search tree, as produced by\n[`MST.kruskal`](/api/MST#kruskal) and [`MST.prim`](/api/MST#prim).\n\n```lua\nlocal edges = MST.kruskal(vertices, getNeighbors, getWeight)\nfor _, edge in edges do\n    print(`{edge.from} -> {edge.to} ({edge.weight})`)\nend\n```",
            "fields": [
                {
                    "name": "from",
                    "lua_type": "V",
                    "desc": "The vertex the edge starts at."
                },
                {
                    "name": "to",
                    "lua_type": "V",
                    "desc": "The vertex the edge ends at."
                },
                {
                    "name": "weight",
                    "lua_type": "number",
                    "desc": "The edge's weight."
                }
            ],
            "source": {
                "line": 247,
                "path": "lib/graphutil/src/Types.luau"
            }
        }
    ],
    "name": "GraphUtil",
    "desc": "A library of generic, graph-agnostic algorithms. Describe your graph with\na few callbacks — no bespoke data structure required — and run any\nalgorithm over any representation (adjacency lists, spatial grids, implicit\nneighbour functions, …).\n\n### Submodules\n\n| Module | Purpose |\n| --- | --- |\n| [BFS](/api/BFS) | Breadth-first traversal & unweighted shortest paths |\n| [DFS](/api/DFS) | Depth-first traversal & spanning trees |\n| [Dijkstra](/api/Dijkstra) | Weighted shortest paths (non-negative) & cost-ordered traversal |\n| [AStar](/api/AStar) | Heuristic-guided weighted shortest path |\n| [BellmanFord](/api/BellmanFord) | Weighted shortest paths with negative edges & cycle detection |\n| [MST](/api/MST) | Minimum spanning trees (Prim / Kruskal) |\n| [TopologicalSort](/api/TopologicalSort) | Linear ordering of a DAG (DFS / Kahn) |\n| [Connectivity](/api/Connectivity) | Reachability, components, cycles, bipartite, SCCs |\n\n### Shared callbacks\n\nAlgorithms are driven by callbacks rather than a fixed structure. See each\ntype page for the full signature and worked examples:\n[GetNeighbors](#GetNeighbors) (required by every algorithm),\n[GetWeight](#GetWeight) (weighted algorithms), and\n[GetHeuristic](#GetHeuristic) (A\\*). Path-finders return a\n[PathResult](#PathResult); MST returns [Edge](#Edge) lists.\n\n### Directed vs. undirected\n\nBecause your callbacks define the edges, most algorithms work on directed\nand undirected graphs alike. Every function carries a `directed` and/or\n`undirected` tag showing which graph kinds it supports — a function tagged\nwith only one (e.g. [TopologicalSort](/api/TopologicalSort) is\ndirected-only, [MST](/api/MST) is undirected-only) gives wrong or\nmeaningless results on the other kind.\n\n```lua\nlocal GraphUtil = require(Packages.GraphUtil)\n\nlocal adjacency = {\n    A = { \"B\", \"C\" },\n    B = { \"A\", \"D\" },\n    C = { \"A\", \"D\" },\n    D = { \"B\", \"C\" },\n}\n\nlocal result = GraphUtil.BFS.shortestPath(\"A\", \"D\", function(v)\n    return adjacency[v]\nend)\n-- result.path == { \"A\", \"B\", \"D\" }\n```",
    "source": {
        "line": 59,
        "path": "lib/graphutil/src/init.luau"
    }
}