Skip to content

Commit 853be79

Browse files
committed
Genericize static analysis of variadic arguments.
1 parent cb4644c commit 853be79

File tree

4 files changed

+111
-95
lines changed

4 files changed

+111
-95
lines changed

config/staticconfig/internal/ssax/value.go

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,46 @@ import (
1212
//
1313
// If v cannot be resolved to a single value, it returns an empty optional.
1414
func StaticValue(v ssa.Value) optional.Optional[ssa.Value] {
15-
switch v := v.(type) {
16-
case *ssa.Const:
17-
return optional.Some[ssa.Value](v)
18-
19-
case ssa.Instruction:
20-
values := staticValuesFromInstruction(v)
21-
if len(values) > 1 {
22-
panic("did not expect multiple values")
23-
}
15+
values := staticValues(v)
16+
if len(values) > 1 {
17+
panic("did not expect multiple values")
18+
}
2419

25-
if len(values) == 1 {
26-
return values[0]
27-
}
20+
if len(values) == 1 {
21+
return values[0]
2822
}
2923

30-
// TODO(jmalloc): This implementation is incomplete.
3124
return optional.None[ssa.Value]()
3225
}
3326

34-
// staticValuesFromInstruction returns the static value(s) that result from
35-
// evaluating the given instruction.
27+
// staticValues returns the static value(s) that result from evaluating the
28+
// given node.
3629
//
3730
// If an individual value within the expression cannot be resolved to a singular
38-
// static value, it is represented as a nil value in the returned slice.
31+
// static value, it is represented as an empty optional in the returned slice.
3932
//
4033
// It returns an empty slice if the expression itself cannot be resolved.
41-
func staticValuesFromInstruction(inst ssa.Instruction) []optional.Optional[ssa.Value] {
42-
switch inst := inst.(type) {
34+
func staticValues(v ssa.Value) []optional.Optional[ssa.Value] {
35+
switch v := v.(type) {
36+
case *ssa.Const:
37+
return optional.Slice[ssa.Value](v)
38+
4339
case *ssa.Call:
44-
return staticValuesFromCall(inst.Common())
40+
return staticValuesFromCall(v.Common())
4541

4642
case *ssa.Extract:
47-
if expr, ok := inst.Tuple.(ssa.Instruction); ok {
48-
values := staticValuesFromInstruction(expr)
49-
return values[inst.Index : inst.Index+1]
43+
values := staticValues(v.Tuple)
44+
if len(values) <= v.Index {
45+
return nil
46+
}
47+
return values[v.Index : v.Index+1]
48+
49+
case *ssa.MakeInterface:
50+
return staticValues(v.X)
51+
52+
case *ssa.UnOp:
53+
if v.Op == token.MUL { // pointer de-reference
54+
return staticValues(v.X)
5055
}
5156
}
5257

@@ -58,13 +63,11 @@ func staticValuesFromInstruction(inst ssa.Instruction) []optional.Optional[ssa.V
5863
// a call to a function.
5964
//
6065
// If an individual value within the expression cannot be resolved to a singular
61-
// static value, it is represented as a nil value in the returned slice.
66+
// static value, it is represented as an empty value in the returned slice.
6267
//
6368
// It returns an empty slice if the function itself cannot be resolved. For
6469
// example, if it is a dynamic call to an interface method.
65-
func staticValuesFromCall(
66-
call *ssa.CallCommon,
67-
) []optional.Optional[ssa.Value] {
70+
func staticValuesFromCall(call *ssa.CallCommon) []optional.Optional[ssa.Value] {
6871
// TODO: we could use StaticValue or some variant thereof to resolve the
6972
// callee in more cases.
7073
fn := call.StaticCallee()

config/staticconfig/route.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@ func analyzeRoutes[
1414
](
1515
ctx *configurerCallContext[T, H, B],
1616
) {
17-
for r := range resolveVariadic(ctx.Builder, ctx.Instruction) {
18-
ctx.Builder.Route(func(b *configbuilder.RouteBuilder) {
19-
if ctx.IsSpeculative {
20-
b.Speculative() // TODO: is this correct?
21-
}
22-
analyzeRoute(ctx.context, b, r)
23-
})
24-
}
17+
analyzeVariadicArguments(
18+
ctx,
19+
ctx.Builder.Route,
20+
analyzeRoute,
21+
)
2522
}
2623

2724
func analyzeRoute(

config/staticconfig/varargs.go

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,92 @@
11
package staticconfig
22

33
import (
4-
"go/token"
5-
"iter"
6-
74
"github.com/dogmatiq/enginekit/config"
85
"github.com/dogmatiq/enginekit/config/internal/configbuilder"
96
"github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax"
107
"golang.org/x/tools/go/ssa"
118
)
129

13-
func findAllocation(v ssa.Value) (*ssa.Alloc, bool) {
14-
switch v := v.(type) {
15-
case *ssa.Alloc:
16-
return v, true
17-
18-
case *ssa.Slice:
19-
return findAllocation(v.X)
20-
21-
case *ssa.UnOp:
22-
if v.Op == token.MUL { // pointer de-reference
23-
return findAllocation(v.X)
24-
}
25-
return nil, false
26-
27-
default:
28-
return nil, false
29-
}
30-
}
31-
32-
func isIndexOfArray(
33-
array *ssa.Alloc,
34-
v ssa.Value,
35-
) (int, bool) {
36-
switch v := v.(type) {
37-
case *ssa.IndexAddr:
38-
if v.X != array {
39-
return 0, false
40-
}
41-
return ssax.AsInt(v.Index).TryGet()
42-
}
43-
return 0, false
44-
}
45-
46-
func resolveVariadic[
10+
func analyzeVariadicArguments[
4711
T config.Entity,
4812
E any,
4913
B configbuilder.EntityBuilder[T, E],
14+
TChild config.Component,
15+
BChild configbuilder.ComponentBuilder[TChild],
5016
](
51-
b B,
52-
inst ssa.CallInstruction,
53-
) iter.Seq[ssa.Value] {
54-
return func(yield func(ssa.Value) bool) {
55-
call := inst.Common()
17+
ctx *configurerCallContext[T, E, B],
18+
child func(func(BChild)),
19+
analyze func(*context, BChild, ssa.Value),
20+
) {
21+
// The variadic slice parameter is always the last argument.
22+
varargs := ctx.Args[len(ctx.Args)-1]
5623

57-
variadics := call.Args[len(call.Args)-1]
58-
if ssax.IsZeroValue(variadics) {
59-
return
60-
}
24+
if ssax.IsZeroValue(varargs) {
25+
return
26+
}
27+
28+
array, ok := findSliceArrayAllocation(varargs)
29+
if !ok {
30+
ctx.Builder.Partial()
31+
return
32+
}
6133

62-
array, ok := findAllocation(variadics)
63-
if !ok {
64-
b.Partial()
65-
return
34+
buildersByIndex := map[int][]BChild{}
35+
36+
for block := range ssax.WalkBlock(array.Block()) {
37+
// If there's no path from this block to the call instruction, we can
38+
// safely ignore it, even if it modifies the underlying array.
39+
if !ssax.PathExists(block, ctx.Instruction.Block()) {
40+
continue
6641
}
6742

68-
for b := range ssax.WalkBlock(array.Block()) {
69-
if !ssax.PathExists(b, inst.Block()) {
70-
continue
71-
}
43+
for inst := range ssax.InstructionsBefore(block, ctx.Instruction) {
44+
switch inst := inst.(type) {
45+
case *ssa.Store:
46+
if addr, ok := inst.Addr.(*ssa.IndexAddr); ok && addr.X == array {
47+
child(func(b BChild) {
48+
if index, ok := ssax.AsInt(addr.Index).TryGet(); ok {
49+
// If there are multiple writes to the same index,
50+
// we mark them all as speculative.
51+
//
52+
// TODO: Could we handle this more intelligently by
53+
// using the value of the store instruction closest
54+
// to the call instruction?
55+
conflicting := buildersByIndex[index]
56+
if len(conflicting) == 1 {
57+
conflicting[0].Speculative()
58+
}
59+
if len(conflicting) != 0 {
60+
b.Speculative()
61+
}
62+
buildersByIndex[index] = append(conflicting, b)
63+
} else {
64+
// If we can't resolve the index we assume the child
65+
// is speculative because we can't tell if it is
66+
// ever overwritten with a different value.
67+
b.Speculative()
68+
}
7269

73-
for inst := range ssax.InstructionsBefore(b, inst) {
74-
switch inst := inst.(type) {
75-
case *ssa.Store:
76-
if _, ok := isIndexOfArray(array, inst.Addr); ok {
77-
if !yield(inst.Val) {
78-
return
70+
if ctx.IsSpeculative {
71+
b.Speculative()
7972
}
80-
}
73+
74+
analyze(ctx.context, b, inst.Val)
75+
})
8176
}
8277
}
8378
}
8479
}
8580
}
81+
82+
// findSliceArrayAllocation returns the underlying array allocation of a slice.
83+
func findSliceArrayAllocation(v ssa.Value) (*ssa.Alloc, bool) {
84+
switch v := v.(type) {
85+
case *ssa.Alloc:
86+
return v, true
87+
case *ssa.Slice:
88+
return findSliceArrayAllocation(v.X)
89+
default:
90+
return nil, false
91+
}
92+
}

optional/container.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ func Key[K comparable, V any, M ~map[K]V](m M, k K) Optional[V] {
3232
}
3333
return None[V]()
3434
}
35+
36+
// Slice returns a slice of Optional[T] values.
37+
func Slice[T any](elems ...T) []Optional[T] {
38+
slice := make([]Optional[T], len(elems))
39+
for i, elem := range elems {
40+
slice[i] = Some(elem)
41+
}
42+
return slice
43+
}

0 commit comments

Comments
 (0)