aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2026-01-23 15:25:16 +0100
committerDmitry Vyukov <dvyukov@google.com>2026-01-24 07:02:54 +0000
commit55a4296bd1fe1c873e4de2d099ab561e5fca592e (patch)
treeb8e7752b90800c8052c5b63a1dd931d0b95421cc /pkg
parent8534bc8373e09185ab613e4b39eb1ee6bf6d180c (diff)
pkg/aflow: add DoWhile loop action
DoWhile represents "do { body } while (cond)" loop. See added test for an example.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/aflow/loop.go98
-rw-r--r--pkg/aflow/loop_test.go96
-rw-r--r--pkg/aflow/testdata/TestDoWhile.trajectory.json191
3 files changed, 385 insertions, 0 deletions
diff --git a/pkg/aflow/loop.go b/pkg/aflow/loop.go
new file mode 100644
index 000000000..e074d4217
--- /dev/null
+++ b/pkg/aflow/loop.go
@@ -0,0 +1,98 @@
+// Copyright 2026 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package aflow
+
+import (
+ "fmt"
+ "maps"
+ "reflect"
+
+ "github.com/google/syzkaller/pkg/aflow/trajectory"
+)
+
+// DoWhile represents "do { body } while (cond)" loop.
+type DoWhile struct {
+ // Dody of the loop.
+ Do Action
+ // Exit condition. It should be a string state variable.
+ // The loop exists when the variable is empty.
+ While string
+
+ loopVars map[string]reflect.Type
+}
+
+func (dw *DoWhile) execute(ctx *Context) error {
+ span := &trajectory.Span{
+ Type: trajectory.SpanLoop,
+ }
+ if err := ctx.startSpan(span); err != nil {
+ return err
+ }
+ err := dw.loop(ctx)
+ if err := ctx.finishSpan(span, err); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (dw *DoWhile) loop(ctx *Context) error {
+ for name, typ := range dw.loopVars {
+ if _, ok := ctx.state[name]; ok {
+ return fmt.Errorf("loop var %q is already defined", name)
+ }
+ ctx.state[name] = reflect.Zero(typ).Interface()
+ }
+ const maxIters = 100
+ for iter := 0; iter < maxIters; iter++ {
+ span := &trajectory.Span{
+ Type: trajectory.SpanLoopIteration,
+ Name: fmt.Sprint(iter),
+ }
+ if err := ctx.startSpan(span); err != nil {
+ return err
+ }
+ err := dw.Do.execute(ctx)
+ if err := ctx.finishSpan(span, err); err != nil {
+ return err
+ }
+ if ctx.state[dw.While].(string) == "" {
+ return nil
+ }
+ }
+ return fmt.Errorf("DoWhile loop is going in cycles for %v iterations", maxIters)
+}
+
+func (dw *DoWhile) verify(ctx *verifyContext) {
+ // Verification of loops is a bit tricky.
+ // Normally we require each variable to be defined before use, but loops violate
+ // the assumption. An action in a loop body may want to use a variable produced
+ // by a subsequent action in the body on the previous iteration (otherwise there
+ // is no way to provide feedback from one iteration to the next iteration).
+ // But on the first iteration that variable is not defined yet. To resolve this,
+ // we split verification into 2 parts: first, all body actions provide outputs,
+ // and we collect all provided outputs in loopVars; second, we verify their inputs
+ // (with all outputs from the whole body already defined). Later, during execution
+ // we will define all loopVars to zero values before starting the loop body.
+ inputs, outputs := ctx.inputs, ctx.outputs
+ defer func() {
+ ctx.inputs, ctx.outputs = inputs, outputs
+ }()
+ if outputs {
+ ctx.inputs, ctx.outputs = false, true
+ origState := maps.Clone(ctx.state)
+ dw.Do.verify(ctx)
+ dw.loopVars = make(map[string]reflect.Type)
+ for name, desc := range ctx.state {
+ if origState[name] == nil {
+ dw.loopVars[name] = desc.typ
+ }
+ }
+ }
+ if inputs {
+ ctx.inputs, ctx.outputs = true, false
+ dw.Do.verify(ctx)
+ ctx.requireNotEmpty("DoWhile", "While", dw.While)
+ ctx.requireInput("DoWhile", dw.While, reflect.TypeFor[string]())
+ }
+}
diff --git a/pkg/aflow/loop_test.go b/pkg/aflow/loop_test.go
new file mode 100644
index 000000000..e68191c2b
--- /dev/null
+++ b/pkg/aflow/loop_test.go
@@ -0,0 +1,96 @@
+// Copyright 2026 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package aflow
+
+import (
+ "testing"
+)
+
+func TestDoWhile(t *testing.T) {
+ type inputs struct {
+ Bug string
+ }
+ type outputs struct {
+ Diff string
+ }
+ type patchArgs struct {
+ Bug string
+ Diff string
+ TestError string
+ }
+ type patchResults struct {
+ Patch string
+ }
+ type testArgs struct {
+ Patch string
+ }
+ type testResults struct {
+ Diff string
+ TestError string
+ }
+ iter := 0
+ testFlow[inputs, outputs](t, map[string]any{"Bug": "bug"}, map[string]any{"Diff": "diff"},
+ &DoWhile{
+ Do: Pipeline(
+ NewFuncAction("patch-generator", func(ctx *Context, args patchArgs) (patchResults, error) {
+ iter++
+ if iter <= 2 {
+ return patchResults{"bad"}, nil
+ }
+ return patchResults{"good"}, nil
+ }),
+ NewFuncAction("patch-tester", func(ctx *Context, args testArgs) (testResults, error) {
+ if args.Patch == "bad" {
+ return testResults{TestError: "error"}, nil
+ }
+ return testResults{Diff: "diff"}, nil
+ }),
+ ),
+ While: "TestError",
+ },
+ nil,
+ )
+}
+
+func TestDoWhileErrors(t *testing.T) {
+ testRegistrationError[struct{}, struct{}](t,
+ "flow test: action body: no input Missing, available inputs: []",
+ Pipeline(
+ &DoWhile{
+ Do: NewFuncAction("body", func(ctx *Context, args struct {
+ Missing string
+ }) (struct{}, error) {
+ return struct{}{}, nil
+ }),
+ While: "Condition",
+ },
+ ))
+
+ testRegistrationError[struct{ Input string }, struct{}](t,
+ "flow test: action DoWhile: While must not be empty",
+ Pipeline(
+ &DoWhile{
+ Do: NewFuncAction("body", func(ctx *Context, args struct {
+ Input string
+ }) (struct{}, error) {
+ return struct{}{}, nil
+ }),
+ },
+ ))
+
+ type output struct {
+ Output1 string
+ Output2 string
+ }
+ testRegistrationError[struct{}, struct{}](t,
+ "flow test: action body: output Output2 is unused",
+ Pipeline(
+ &DoWhile{
+ Do: NewFuncAction("body", func(ctx *Context, args struct{}) (output, error) {
+ return output{}, nil
+ }),
+ While: "Output1",
+ },
+ ))
+}
diff --git a/pkg/aflow/testdata/TestDoWhile.trajectory.json b/pkg/aflow/testdata/TestDoWhile.trajectory.json
new file mode 100644
index 000000000..6375d2f95
--- /dev/null
+++ b/pkg/aflow/testdata/TestDoWhile.trajectory.json
@@ -0,0 +1,191 @@
+[
+ {
+ "Seq": 0,
+ "Nesting": 0,
+ "Type": "flow",
+ "Name": "test",
+ "Started": "0001-01-01T00:00:01Z"
+ },
+ {
+ "Seq": 1,
+ "Nesting": 1,
+ "Type": "loop",
+ "Name": "",
+ "Started": "0001-01-01T00:00:02Z"
+ },
+ {
+ "Seq": 2,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "0",
+ "Started": "0001-01-01T00:00:03Z"
+ },
+ {
+ "Seq": 3,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:04Z"
+ },
+ {
+ "Seq": 3,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:04Z",
+ "Finished": "0001-01-01T00:00:05Z",
+ "Results": {
+ "Patch": "bad"
+ }
+ },
+ {
+ "Seq": 4,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:06Z"
+ },
+ {
+ "Seq": 4,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:06Z",
+ "Finished": "0001-01-01T00:00:07Z",
+ "Results": {
+ "Diff": "",
+ "TestError": "error"
+ }
+ },
+ {
+ "Seq": 2,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "0",
+ "Started": "0001-01-01T00:00:03Z",
+ "Finished": "0001-01-01T00:00:08Z"
+ },
+ {
+ "Seq": 5,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "1",
+ "Started": "0001-01-01T00:00:09Z"
+ },
+ {
+ "Seq": 6,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:10Z"
+ },
+ {
+ "Seq": 6,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:10Z",
+ "Finished": "0001-01-01T00:00:11Z",
+ "Results": {
+ "Patch": "bad"
+ }
+ },
+ {
+ "Seq": 7,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:12Z"
+ },
+ {
+ "Seq": 7,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:12Z",
+ "Finished": "0001-01-01T00:00:13Z",
+ "Results": {
+ "Diff": "",
+ "TestError": "error"
+ }
+ },
+ {
+ "Seq": 5,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "1",
+ "Started": "0001-01-01T00:00:09Z",
+ "Finished": "0001-01-01T00:00:14Z"
+ },
+ {
+ "Seq": 8,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "2",
+ "Started": "0001-01-01T00:00:15Z"
+ },
+ {
+ "Seq": 9,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:16Z"
+ },
+ {
+ "Seq": 9,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-generator",
+ "Started": "0001-01-01T00:00:16Z",
+ "Finished": "0001-01-01T00:00:17Z",
+ "Results": {
+ "Patch": "good"
+ }
+ },
+ {
+ "Seq": 10,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:18Z"
+ },
+ {
+ "Seq": 10,
+ "Nesting": 3,
+ "Type": "action",
+ "Name": "patch-tester",
+ "Started": "0001-01-01T00:00:18Z",
+ "Finished": "0001-01-01T00:00:19Z",
+ "Results": {
+ "Diff": "diff",
+ "TestError": ""
+ }
+ },
+ {
+ "Seq": 8,
+ "Nesting": 2,
+ "Type": "iteration",
+ "Name": "2",
+ "Started": "0001-01-01T00:00:15Z",
+ "Finished": "0001-01-01T00:00:20Z"
+ },
+ {
+ "Seq": 1,
+ "Nesting": 1,
+ "Type": "loop",
+ "Name": "",
+ "Started": "0001-01-01T00:00:02Z",
+ "Finished": "0001-01-01T00:00:21Z"
+ },
+ {
+ "Seq": 0,
+ "Nesting": 0,
+ "Type": "flow",
+ "Name": "test",
+ "Started": "0001-01-01T00:00:01Z",
+ "Finished": "0001-01-01T00:00:22Z",
+ "Results": {
+ "Diff": "diff"
+ }
+ }
+] \ No newline at end of file