-
Notifications
You must be signed in to change notification settings - Fork 96
Feat/loop #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/loop #99
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,18 @@ func WithMiddleware(ms ...Middleware) Option { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeCondition is a function that determines if an edge should be followed based on the current state. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type EdgeCondition func(ctx context.Context, state State) bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeType defines the type of an edge in the graph. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type EdgeType int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeTypeNormal is a regular edge (default). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EdgeTypeNormal EdgeType = iota | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeTypeLoop is a back edge that forms a loop - allows revisiting nodes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EdgeTypeLoop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeTypeExit is an exit edge from a loop - required for loop nodes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EdgeTypeExit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EdgeOption configures an edge before it is added to the graph. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type EdgeOption func(*conditionalEdge) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,10 +48,18 @@ func WithEdgeCondition(condition EdgeCondition) EdgeOption { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // conditionalEdge represents an edge with an optional condition. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // WithEdgeType sets the type of the edge (Normal, Loop, or Exit). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func WithEdgeType(edgeType EdgeType) EdgeOption { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return func(edge *conditionalEdge) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| edge.edgeType = edgeType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // conditionalEdge represents an edge with an optional condition and type. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type conditionalEdge struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| condition EdgeCondition // nil means always follow this edge | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| edgeType EdgeType // type of edge (normal, loop, or exit) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Graph represents a directed graph of processing nodes. Cycles are allowed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -161,10 +181,53 @@ func (g *Graph) validateStructure() error { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("graph: node '%s' has mixed conditional and unconditional edges", from) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate loop/exit edge rules | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := g.validateLoopEdges(); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // validateLoopEdges ensures that nodes with loop edges also have exit edges, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and that loop/exit edges have proper conditions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (g *Graph) validateLoopEdges() error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for from, edges := range g.edges { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var loopEdges []conditionalEdge | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var exitEdges []conditionalEdge | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, edge := range edges { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch edge.edgeType { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case EdgeTypeLoop: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loopEdges = append(loopEdges, edge) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case EdgeTypeExit: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exitEdges = append(exitEdges, edge) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If node has loop edges, it must have exit edges | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(loopEdges) > 0 && len(exitEdges) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("graph: node '%s' has loop edges but no exit edges", from) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // All loop and exit edges must have conditions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, edge := range loopEdges { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if edge.condition == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("graph: loop edge from '%s' to '%s' must have a condition", from, edge.to) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, edge := range exitEdges { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if edge.condition == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("graph: exit edge from '%s' to '%s' must have a condition", from, edge.to) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ensureReachable verifies that the finish node can be reached from the entry node. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Loop edges are skipped during reachability check since they don't advance toward the finish. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (g *Graph) ensureReachable() error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if g.entryPoint == g.finishPoint { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -182,13 +245,18 @@ func (g *Graph) ensureReachable() error { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, edge := range g.edges[node] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip loop edges during reachability check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if edge.edgeType == EdgeTypeLoop { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queue = append(queue, edge.to) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("graph: finish node not reachable: %s", g.finishPoint) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ensureAcyclic verifies that the graph does not contain directed cycles. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ensureAcyclic verifies that the graph does not contain directed cycles, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // unless the cycle includes at least one edge marked as EdgeTypeLoop. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (g *Graph) ensureAcyclic() error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stateUnvisited = iota | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -198,26 +266,32 @@ func (g *Graph) ensureAcyclic() error { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| states := make(map[string]int, len(g.nodes)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stack := make([]string, 0, len(g.nodes)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var visit func(string) error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visit = func(node string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var visit func(string, bool) error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visit = func(node string, hasLoopEdge bool) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| states[node] = stateVisiting | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stack = append(stack, node) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, edge := range g.edges[node] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next := edge.to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nextHasLoop := hasLoopEdge || edge.edgeType == EdgeTypeLoop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch states[next] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case stateVisiting: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cycleStart := 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, name := range stack { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if name == next { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cycleStart = i | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Found a back edge (cycle) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Allow it only if the cycle includes a loop edge | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !nextHasLoop { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cycleStart := 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, name := range stack { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if name == next { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cycleStart = i | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cycle := append(append([]string{}, stack[cycleStart:]...), next) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+282
to
+291
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Allow it only if the cycle includes a loop edge | |
| if !nextHasLoop { | |
| cycleStart := 0 | |
| for i, name := range stack { | |
| if name == next { | |
| cycleStart = i | |
| break | |
| } | |
| } | |
| cycle := append(append([]string{}, stack[cycleStart:]...), next) | |
| // Allow it only if the cycle itself includes a loop edge | |
| cycleStart := 0 | |
| for i, name := range stack { | |
| if name == next { | |
| cycleStart = i | |
| break | |
| } | |
| } | |
| cycle := append(append([]string{}, stack[cycleStart:]...), next) | |
| hasLoop := false | |
| // Check each edge in the cycle for EdgeTypeLoop | |
| for i := 0; i < len(cycle)-1; i++ { | |
| from := cycle[i] | |
| to := cycle[i+1] | |
| for _, e := range g.edges[from] { | |
| if e.to == to && e.edgeType == EdgeTypeLoop { | |
| hasLoop = true | |
| break | |
| } | |
| } | |
| if hasLoop { | |
| break | |
| } | |
| } | |
| if !hasLoop { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
hasLoopEdgetracking logic is incorrect.hasLoopEdgeshould only be true if there's a loop edge in the path from the root to the current node, not just anywhere in the traversal. The current implementation propagateshasLoopEdgealong all paths once any loop edge is encountered, which means even paths that don't contain a loop edge will be marked as having one.This could allow cycles that don't actually contain a loop edge to pass validation. The fix should track whether the current cycle path contains a loop edge, not whether any path from the root contained one.