Skip to content

Commit 55a4296

Browse files
committed
pkg/aflow: add DoWhile loop action
DoWhile represents "do { body } while (cond)" loop. See added test for an example.
1 parent 8534bc8 commit 55a4296

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed

pkg/aflow/loop.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aflow
5+
6+
import (
7+
"fmt"
8+
"maps"
9+
"reflect"
10+
11+
"github.com/google/syzkaller/pkg/aflow/trajectory"
12+
)
13+
14+
// DoWhile represents "do { body } while (cond)" loop.
15+
type DoWhile struct {
16+
// Dody of the loop.
17+
Do Action
18+
// Exit condition. It should be a string state variable.
19+
// The loop exists when the variable is empty.
20+
While string
21+
22+
loopVars map[string]reflect.Type
23+
}
24+
25+
func (dw *DoWhile) execute(ctx *Context) error {
26+
span := &trajectory.Span{
27+
Type: trajectory.SpanLoop,
28+
}
29+
if err := ctx.startSpan(span); err != nil {
30+
return err
31+
}
32+
err := dw.loop(ctx)
33+
if err := ctx.finishSpan(span, err); err != nil {
34+
return err
35+
}
36+
return nil
37+
}
38+
39+
func (dw *DoWhile) loop(ctx *Context) error {
40+
for name, typ := range dw.loopVars {
41+
if _, ok := ctx.state[name]; ok {
42+
return fmt.Errorf("loop var %q is already defined", name)
43+
}
44+
ctx.state[name] = reflect.Zero(typ).Interface()
45+
}
46+
const maxIters = 100
47+
for iter := 0; iter < maxIters; iter++ {
48+
span := &trajectory.Span{
49+
Type: trajectory.SpanLoopIteration,
50+
Name: fmt.Sprint(iter),
51+
}
52+
if err := ctx.startSpan(span); err != nil {
53+
return err
54+
}
55+
err := dw.Do.execute(ctx)
56+
if err := ctx.finishSpan(span, err); err != nil {
57+
return err
58+
}
59+
if ctx.state[dw.While].(string) == "" {
60+
return nil
61+
}
62+
}
63+
return fmt.Errorf("DoWhile loop is going in cycles for %v iterations", maxIters)
64+
}
65+
66+
func (dw *DoWhile) verify(ctx *verifyContext) {
67+
// Verification of loops is a bit tricky.
68+
// Normally we require each variable to be defined before use, but loops violate
69+
// the assumption. An action in a loop body may want to use a variable produced
70+
// by a subsequent action in the body on the previous iteration (otherwise there
71+
// is no way to provide feedback from one iteration to the next iteration).
72+
// But on the first iteration that variable is not defined yet. To resolve this,
73+
// we split verification into 2 parts: first, all body actions provide outputs,
74+
// and we collect all provided outputs in loopVars; second, we verify their inputs
75+
// (with all outputs from the whole body already defined). Later, during execution
76+
// we will define all loopVars to zero values before starting the loop body.
77+
inputs, outputs := ctx.inputs, ctx.outputs
78+
defer func() {
79+
ctx.inputs, ctx.outputs = inputs, outputs
80+
}()
81+
if outputs {
82+
ctx.inputs, ctx.outputs = false, true
83+
origState := maps.Clone(ctx.state)
84+
dw.Do.verify(ctx)
85+
dw.loopVars = make(map[string]reflect.Type)
86+
for name, desc := range ctx.state {
87+
if origState[name] == nil {
88+
dw.loopVars[name] = desc.typ
89+
}
90+
}
91+
}
92+
if inputs {
93+
ctx.inputs, ctx.outputs = true, false
94+
dw.Do.verify(ctx)
95+
ctx.requireNotEmpty("DoWhile", "While", dw.While)
96+
ctx.requireInput("DoWhile", dw.While, reflect.TypeFor[string]())
97+
}
98+
}

pkg/aflow/loop_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2026 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aflow
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestDoWhile(t *testing.T) {
11+
type inputs struct {
12+
Bug string
13+
}
14+
type outputs struct {
15+
Diff string
16+
}
17+
type patchArgs struct {
18+
Bug string
19+
Diff string
20+
TestError string
21+
}
22+
type patchResults struct {
23+
Patch string
24+
}
25+
type testArgs struct {
26+
Patch string
27+
}
28+
type testResults struct {
29+
Diff string
30+
TestError string
31+
}
32+
iter := 0
33+
testFlow[inputs, outputs](t, map[string]any{"Bug": "bug"}, map[string]any{"Diff": "diff"},
34+
&DoWhile{
35+
Do: Pipeline(
36+
NewFuncAction("patch-generator", func(ctx *Context, args patchArgs) (patchResults, error) {
37+
iter++
38+
if iter <= 2 {
39+
return patchResults{"bad"}, nil
40+
}
41+
return patchResults{"good"}, nil
42+
}),
43+
NewFuncAction("patch-tester", func(ctx *Context, args testArgs) (testResults, error) {
44+
if args.Patch == "bad" {
45+
return testResults{TestError: "error"}, nil
46+
}
47+
return testResults{Diff: "diff"}, nil
48+
}),
49+
),
50+
While: "TestError",
51+
},
52+
nil,
53+
)
54+
}
55+
56+
func TestDoWhileErrors(t *testing.T) {
57+
testRegistrationError[struct{}, struct{}](t,
58+
"flow test: action body: no input Missing, available inputs: []",
59+
Pipeline(
60+
&DoWhile{
61+
Do: NewFuncAction("body", func(ctx *Context, args struct {
62+
Missing string
63+
}) (struct{}, error) {
64+
return struct{}{}, nil
65+
}),
66+
While: "Condition",
67+
},
68+
))
69+
70+
testRegistrationError[struct{ Input string }, struct{}](t,
71+
"flow test: action DoWhile: While must not be empty",
72+
Pipeline(
73+
&DoWhile{
74+
Do: NewFuncAction("body", func(ctx *Context, args struct {
75+
Input string
76+
}) (struct{}, error) {
77+
return struct{}{}, nil
78+
}),
79+
},
80+
))
81+
82+
type output struct {
83+
Output1 string
84+
Output2 string
85+
}
86+
testRegistrationError[struct{}, struct{}](t,
87+
"flow test: action body: output Output2 is unused",
88+
Pipeline(
89+
&DoWhile{
90+
Do: NewFuncAction("body", func(ctx *Context, args struct{}) (output, error) {
91+
return output{}, nil
92+
}),
93+
While: "Output1",
94+
},
95+
))
96+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
[
2+
{
3+
"Seq": 0,
4+
"Nesting": 0,
5+
"Type": "flow",
6+
"Name": "test",
7+
"Started": "0001-01-01T00:00:01Z"
8+
},
9+
{
10+
"Seq": 1,
11+
"Nesting": 1,
12+
"Type": "loop",
13+
"Name": "",
14+
"Started": "0001-01-01T00:00:02Z"
15+
},
16+
{
17+
"Seq": 2,
18+
"Nesting": 2,
19+
"Type": "iteration",
20+
"Name": "0",
21+
"Started": "0001-01-01T00:00:03Z"
22+
},
23+
{
24+
"Seq": 3,
25+
"Nesting": 3,
26+
"Type": "action",
27+
"Name": "patch-generator",
28+
"Started": "0001-01-01T00:00:04Z"
29+
},
30+
{
31+
"Seq": 3,
32+
"Nesting": 3,
33+
"Type": "action",
34+
"Name": "patch-generator",
35+
"Started": "0001-01-01T00:00:04Z",
36+
"Finished": "0001-01-01T00:00:05Z",
37+
"Results": {
38+
"Patch": "bad"
39+
}
40+
},
41+
{
42+
"Seq": 4,
43+
"Nesting": 3,
44+
"Type": "action",
45+
"Name": "patch-tester",
46+
"Started": "0001-01-01T00:00:06Z"
47+
},
48+
{
49+
"Seq": 4,
50+
"Nesting": 3,
51+
"Type": "action",
52+
"Name": "patch-tester",
53+
"Started": "0001-01-01T00:00:06Z",
54+
"Finished": "0001-01-01T00:00:07Z",
55+
"Results": {
56+
"Diff": "",
57+
"TestError": "error"
58+
}
59+
},
60+
{
61+
"Seq": 2,
62+
"Nesting": 2,
63+
"Type": "iteration",
64+
"Name": "0",
65+
"Started": "0001-01-01T00:00:03Z",
66+
"Finished": "0001-01-01T00:00:08Z"
67+
},
68+
{
69+
"Seq": 5,
70+
"Nesting": 2,
71+
"Type": "iteration",
72+
"Name": "1",
73+
"Started": "0001-01-01T00:00:09Z"
74+
},
75+
{
76+
"Seq": 6,
77+
"Nesting": 3,
78+
"Type": "action",
79+
"Name": "patch-generator",
80+
"Started": "0001-01-01T00:00:10Z"
81+
},
82+
{
83+
"Seq": 6,
84+
"Nesting": 3,
85+
"Type": "action",
86+
"Name": "patch-generator",
87+
"Started": "0001-01-01T00:00:10Z",
88+
"Finished": "0001-01-01T00:00:11Z",
89+
"Results": {
90+
"Patch": "bad"
91+
}
92+
},
93+
{
94+
"Seq": 7,
95+
"Nesting": 3,
96+
"Type": "action",
97+
"Name": "patch-tester",
98+
"Started": "0001-01-01T00:00:12Z"
99+
},
100+
{
101+
"Seq": 7,
102+
"Nesting": 3,
103+
"Type": "action",
104+
"Name": "patch-tester",
105+
"Started": "0001-01-01T00:00:12Z",
106+
"Finished": "0001-01-01T00:00:13Z",
107+
"Results": {
108+
"Diff": "",
109+
"TestError": "error"
110+
}
111+
},
112+
{
113+
"Seq": 5,
114+
"Nesting": 2,
115+
"Type": "iteration",
116+
"Name": "1",
117+
"Started": "0001-01-01T00:00:09Z",
118+
"Finished": "0001-01-01T00:00:14Z"
119+
},
120+
{
121+
"Seq": 8,
122+
"Nesting": 2,
123+
"Type": "iteration",
124+
"Name": "2",
125+
"Started": "0001-01-01T00:00:15Z"
126+
},
127+
{
128+
"Seq": 9,
129+
"Nesting": 3,
130+
"Type": "action",
131+
"Name": "patch-generator",
132+
"Started": "0001-01-01T00:00:16Z"
133+
},
134+
{
135+
"Seq": 9,
136+
"Nesting": 3,
137+
"Type": "action",
138+
"Name": "patch-generator",
139+
"Started": "0001-01-01T00:00:16Z",
140+
"Finished": "0001-01-01T00:00:17Z",
141+
"Results": {
142+
"Patch": "good"
143+
}
144+
},
145+
{
146+
"Seq": 10,
147+
"Nesting": 3,
148+
"Type": "action",
149+
"Name": "patch-tester",
150+
"Started": "0001-01-01T00:00:18Z"
151+
},
152+
{
153+
"Seq": 10,
154+
"Nesting": 3,
155+
"Type": "action",
156+
"Name": "patch-tester",
157+
"Started": "0001-01-01T00:00:18Z",
158+
"Finished": "0001-01-01T00:00:19Z",
159+
"Results": {
160+
"Diff": "diff",
161+
"TestError": ""
162+
}
163+
},
164+
{
165+
"Seq": 8,
166+
"Nesting": 2,
167+
"Type": "iteration",
168+
"Name": "2",
169+
"Started": "0001-01-01T00:00:15Z",
170+
"Finished": "0001-01-01T00:00:20Z"
171+
},
172+
{
173+
"Seq": 1,
174+
"Nesting": 1,
175+
"Type": "loop",
176+
"Name": "",
177+
"Started": "0001-01-01T00:00:02Z",
178+
"Finished": "0001-01-01T00:00:21Z"
179+
},
180+
{
181+
"Seq": 0,
182+
"Nesting": 0,
183+
"Type": "flow",
184+
"Name": "test",
185+
"Started": "0001-01-01T00:00:01Z",
186+
"Finished": "0001-01-01T00:00:22Z",
187+
"Results": {
188+
"Diff": "diff"
189+
}
190+
}
191+
]

0 commit comments

Comments
 (0)