compose: fan-in merge panics nondeterministically when a predecessor returns (nil, nil)
- Dominant language
- Go
- Stars
- 13k
- Forks
- 1.1k
- Avg merge
- 4h 6m
- Merged PRs (30d)
- 41
Description
## 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** with `reflect: call of reflect.Value.Type on zero Value` (re-panicked out of `Invoke` via `graph_run.go:112`);
- otherwise: the type-specific merge func fails with e.g. `(values merge map) field type mismatch. expected: 'map[string]interface {}', got: ''`.
Same compiled graph, same input — panic or error depending on map iteration order.
## To Reproduce
```go
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:
```go
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) // panic
```
## Expected 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.
Contributor guide
Research direction
Read compose/values_merge.go:40-41 and graph_run.go:112 first, then reproduce the unit cases and TestNilFanIn shown in the issue. Done means fan-in handling is deterministic for nil and non-nil values, all-nil merges return nil, and the unit and graph-level regression tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100