aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/aflow/loop.go
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/aflow/loop.go
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/aflow/loop.go')
-rw-r--r--pkg/aflow/loop.go98
1 files changed, 98 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]())
+ }
+}