compose: fan-in merge panics nondeterministically when a predecessor returns (nil, nil)
Summary
When a node in a fan-in returns (nil, nil), mergeValues (compose/values_merge.go:40-41) receives the untyped nil among the values to merge. Channel Values are stored in a Go map, so iteration order is random:
- when the nil lands at
vs[0]:reflect.ValueOf(vs[0]).Type()panics withreflect: call of reflect.Value.Type on zero Value(re-panicked out ofInvokeviagraph_run.go:112); - otherwise: the type-specific merge func fails with e.g.
(values merge map) field type mismatch. expected: 'map[string]interface {}', got: '<nil>'.
Same compiled graph, same input — panic or error depending on map iteration order.
To Reproduce
func TestNilFanIn(t *testing.T) {
for i := 0; i < 30; i++ {
g := compose.NewGraph[string, any]()
g.AddLambdaNode("nilnode", compose.InvokableLambda(func(ctx context.Context, in string) (any, error) {
return nil, nil
}))
g.AddLambdaNode("mapnode", compose.InvokableLambda(func(ctx context.Context, in string) (any, error) {
return map[string]any{"k": "v"}, nil
}))
g.AddLambdaNode("sink", compose.InvokableLambda(func(ctx context.Context, in any) (any, error) {
return in, nil
}))
g.AddEdge(compose.START, "nilnode")
g.AddEdge(compose.START, "mapnode")
g.AddEdge("nilnode", "sink")
g.AddEdge("mapnode", "sink")
g.AddEdge("sink", compose.END)
r, _ := g.Compile(context.Background())
_, _ = r.Invoke(context.Background(), "x") // panics on some iterations
}
}Unit-level, both forms reproduce deterministically:
mergeValues([]any{nil, map[string]any{"k": "v"}}, nil) // panic
mergeValues([]any{map[string]any{"k": "v"}, nil}, nil) // field type mismatch error
mergeValues([]any{nil, nil}, nil) // panicExpected behavior
An untyped nil output carries no value: the merge should deterministically equal the merge of the remaining values (all-nil merges to nil), instead of panicking or erroring at random.
Note
This is distinct from the runtime edge type check: an any-typed predecessor's nil output flowing into a map[string]any-typed successor fails the assignableTypeMay edge check with a clear, deterministic error — that behavior is intentional and unchanged. This issue is specifically about the fan-in merge of values that legitimately reached the merge step.
Version
Current main (9d983b36). I have a fix with unit + graph-level regression tests ready and will open a PR shortly.
Source: cloudwego/eino