--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file graph.lua -- -- load modules local table = require("base/table") local queue = require("base/queue") local object = require("base/object") local hashset = require("base/hashset") local utils = require("base/utils") -- define module local graph = graph or object { _init = {"_directed"} } {true} local edge = edge or object { _init = {"_from", "_to"} } -- new edge, from -> to function edge.new(from, to) return edge {from, to} end function edge:from() return self._from end function edge:to() return self._to end function edge:other(v) if v == self._from then return self._to else return self._from end end function edge:__tostring() return string.format("", self:from(), self:to()) end -- clear graph function graph:clear() self._vertices = {} self._edges = {} self._adjacent_edges = {} self._edges_map = {} -- clear partial topological sort state self:partial_topo_sort_reset() end -- is empty? function graph:empty() return #self:vertices() == 0 end -- is directed? function graph:is_directed() return self._directed end -- get vertices function graph:vertices() return self._vertices end -- get adjacent edges of the the given vertex function graph:adjacent_edges(v) return self._adjacent_edges[v] end -- get the vertex at the given index function graph:vertex(idx) return self:vertices()[idx] end -- has the given vertex? function graph:has_vertex(v) return self._adjacent_edges[v] ~= nil end -- add an isolated without edges function graph:add_vertex(v) if not self:has_vertex(v) then table.insert(self._vertices, v) self._adjacent_edges[v] = {} end -- reset partial topological sort state since graph structure changed self._partial_topo_dirty = true end -- remove the given vertex? function graph:remove_vertex(v) local contains = false table.remove_if(self._vertices, function (_, item) if item == v then contains = true return true end end) if contains then -- remove all touching edges local touching = {} for _, e in ipairs(self._edges) do if e:from() == v or e:to() == v then table.insert(touching, {e:from(), e:to()}) end end for _, pair in ipairs(touching) do self:remove_edge(pair[1], pair[2]) end self._edges_map[v] = nil self._adjacent_edges[v] = nil -- reset partial topological sort state since graph structure changed self._partial_topo_dirty = true end end -- reset partial topological sort state function graph:partial_topo_sort_reset() self._partial_topo_in_progress = false self._partial_topo_in_degree = nil self._partial_topo_queue = nil self._partial_topo_processed = nil self._partial_topo_finished = 0 self._partial_topo_has_cycle = nil self._partial_topo_dirty = false end -- get next node in topological order -- -- @param limit the maximum number of nodes to return -- @return array of nodes with zero in-degree, empty when complete -- @return has_cycle indicates if a cycle was detected -- -- @code -- dag:partial_topo_sort_reset() -- -- local node, has_cycle -- local order_vertices = {} -- while true do -- node, has_cycle = dag:partial_topo_sort_next() -- if node then -- table.insert(order_vertices, node) -- dag:partial_topo_sort_remove(node) -- else -- if has_cycle then -- -- find cycle -- end -- break -- end -- end -- @endcode -- -- e.g. -- -- edges: a (indegree: 0) -> b -> c -- -- add_edge(a, b) -- add_edge(b, c) -- -- local node1, has_cycle = g:partial_topo_sort_next() -- return a -- local node2, has_cycle = g:partial_topo_sort_next() -- return b -- local node3, has_cycle = g:partial_topo_sort_next() -- return c -- local node4, has_cycle = g:partial_topo_sort_next() -- return nil (empty, all done) -- function graph:partial_topo_sort_next() -- recompute all nodes if has dirty nodes if self._partial_topo_dirty then self:_partial_topo_sort_recompute_dirty() end -- check if we already detected a cycle if self._partial_topo_has_cycle then return nil, true end -- initialize topological sort state if not already in progress if not self._partial_topo_in_progress then if not self:_partial_topo_sort_init() then return nil, false end self._partial_topo_in_progress = true end -- get one node with zero in-degree local node local partial_topo_queue = self._partial_topo_queue local partial_topo_processed = self._partial_topo_processed while not partial_topo_queue:empty() do local v = partial_topo_queue:pop() if partial_topo_processed:has(v) then self:partial_topo_sort_remove(v) else node = v partial_topo_processed:insert(node) break end end return node, self._partial_topo_has_cycle end -- remove node and update in-degrees based on the nodes in this node function graph:partial_topo_sort_remove(node) if node == nil then return end self._partial_topo_finished = self._partial_topo_finished + 1 local edges = self:adjacent_edges(node) if edges then local partial_topo_in_degree = self._partial_topo_in_degree local partial_topo_queue = self._partial_topo_queue for _, e in ipairs(edges) do if e:from() == node then local w = e:to() local in_degree = partial_topo_in_degree[w] - 1 partial_topo_in_degree[w] = in_degree if in_degree == 0 then partial_topo_queue:push(w) end end end end if self._partial_topo_queue:empty() and self._partial_topo_processed:size() == self._partial_topo_finished then self._partial_topo_has_cycle = self._partial_topo_finished ~= #self:vertices() end end -- topological sort, use kahn's algorithm -- -- e.g. -- -- edges: a (indegree: 0) -> b -> c -- -- add_edge(a, b) -- add_edge(b, c) -- -- it will return {a, b, c} function graph:topo_sort() if not self:is_directed() then return end -- calculate in-degree for each vertex local in_degree = {} for _, v in ipairs(self:vertices()) do in_degree[v] = 0 end -- count incoming edges for each vertex for _, v in ipairs(self:vertices()) do local edges = self:adjacent_edges(v) if edges then for _, e in ipairs(edges) do if e:from() == v then local w = e:to() in_degree[w] = (in_degree[w] or 0) + 1 end end end end -- queue of vertices with no incoming edges (no dependencies) local queue = queue.new() for _, v in ipairs(self:vertices()) do if in_degree[v] == 0 then queue:push(v) end end -- process queue local order_vertices = {} while not queue:empty() do -- remove a vertex with no incoming edges local v = queue:pop() table.insert(order_vertices, v) -- for each outgoing edge, remove it and update in-degrees local edges = self:adjacent_edges(v) if edges then for _, e in ipairs(edges) do if e:from() == v then local w = e:to() local d = in_degree[w] - 1 in_degree[w] = d if d == 0 then queue:push(w) end end end end end -- if we couldn't process all vertices, there must be a cycle local has_cycle = #order_vertices ~= #self:vertices() return order_vertices, has_cycle end -- deprecated function graph:topological_sort() return self:topo_sort() end -- find cycle function graph:find_cycle() local visited = {} local stack = {} local cycle = {} local function dfs(v) visited[v] = true stack[v] = true table.insert(cycle, v) local edges = self:adjacent_edges(v) if edges then for _, e in ipairs(edges) do local w = e:other(v) if not visited[w] then if dfs(w) then return true elseif stack[w] then return true end elseif stack[w] then for i = #cycle, 1, -1 do if cycle[i] == w then cycle = table.slice(cycle, i) return true end end end end end table.remove(cycle) stack[v] = false return false end for _, v in ipairs(self:vertices()) do if not visited[v] then if dfs(v) then return cycle end end end end -- get edges function graph:edges() return self._edges end -- add edge function graph:add_edge(from, to) if self:has_edge(from, to) then return end self:add_vertex(from) self:add_vertex(to) local e = edge.new(from, to) local edges_map = self._edges_map edges_map[from] = edges_map[from] or {} edges_map[from][to] = true if self:is_directed() then table.insert(self._adjacent_edges[from], e) else edges_map[to] = edges_map[to] or {} edges_map[to][from] = true table.insert(self._adjacent_edges[from], e) table.insert(self._adjacent_edges[to], e) end table.insert(self._edges, e) -- reset partial topological sort state since graph structure changed self._partial_topo_dirty = true end -- remove edge function graph:remove_edge(from, to) if not self:has_edge(from, to) then return end local directed = self:is_directed() local edges = self._edges local target_index local target_edge for idx, e in ipairs(edges) do local match = (e:from() == from and e:to() == to) if not directed then match = match or (e:from() == to and e:to() == from) end if match then target_index = idx target_edge = e break end end assert(target_edge, string.format("graph.remove_edge(%s, %s): edge not found", tostring(from), tostring(to))) table.remove(edges, target_index) local function remove_from_adj(vertex) local adj = self._adjacent_edges[vertex] if adj then table.remove_if(adj, function (_, e) return e == target_edge end) end end remove_from_adj(target_edge:from()) remove_from_adj(target_edge:to()) local edges_map = self._edges_map local function clear_map(u, v) local map = edges_map[u] if map then map[v] = nil if not next(map) then edges_map[u] = nil end end end clear_map(target_edge:from(), target_edge:to()) if not directed then clear_map(target_edge:to(), target_edge:from()) end self._partial_topo_dirty = true end -- has the given edge? function graph:has_edge(from, to) local edges_map = self._edges_map local from_map = edges_map[from] if self:is_directed() then return from_map and from_map[to] else local to_map = edges_map[to] return from_map and to_map and from_map[to] and to_map[from] end end -- clone graph function graph:clone() local gh = graph.new(self:is_directed()) for _, e in ipairs(self._edges) do gh:add_edge(e:from(), e:to()) end return gh end -- reverse graph function graph:reverse() if not self:is_directed() then return self:clone() end local gh = graph.new(self:is_directed()) for _, e in ipairs(self._edges) do gh:add_edge(e:to(), e:from()) end return gh end -- dump graph function graph:dump() local vertices = self:vertices() local edges = self:edges() utils.cprint("graph: %s, vertices: %d, edges: %d", self:is_directed() and "directed" or "not-directed", #vertices, #edges) utils.cprint("vertices: ") for _, v in ipairs(vertices) do utils.cprint(" %s", v) end utils.cprint("") utils.cprint("edges: ") for _, e in ipairs(edges) do utils.cprint(" %s ${color.dump.reference}->${clear} %s", e:from(), e:to()) end end -- initialize topological sort state if not already in progress function graph:_partial_topo_sort_init() if not self:is_directed() then return false end -- calculate in-degree for each vertex self._partial_topo_in_degree = {} for _, v in ipairs(self:vertices()) do self._partial_topo_in_degree[v] = 0 end -- count incoming edges for each vertex local partial_topo_in_degree = self._partial_topo_in_degree for _, v in ipairs(self:vertices()) do local edges = self:adjacent_edges(v) if edges then for _, e in ipairs(edges) do if e:from() == v then local w = e:to() partial_topo_in_degree[w] = (partial_topo_in_degree[w] or 0) + 1 end end end end -- initialize queue with vertices that have no incoming edges self._partial_topo_queue = queue.new() local partial_topo_queue = self._partial_topo_queue for _, v in ipairs(self:vertices()) do if partial_topo_in_degree[v] == 0 then partial_topo_queue:push(v) end end self._partial_topo_processed = self._partial_topo_processed or hashset.new() return true end -- recompute all dirty nodes -- -- TODO we recompute all nodes now, but we should optimize to recompute only dirty nodes function graph:_partial_topo_sort_recompute_dirty() self._partial_topo_in_progress = false self._partial_topo_in_degree = nil self._partial_topo_queue = nil self._partial_topo_finished = 0 self._partial_topo_has_cycle = nil self._partial_topo_dirty = false end -- create a new graph -- -- @param directed create a directed graph if true, undirected if false -- @return the graph instance -- function graph.new(directed) local gh = graph {directed} gh:clear() return gh end -- return module: graph return graph