Skip to content

Commit 35fc856

Browse files
authored
acc: allocation resilient to aliased input/output (#134)
Updates the variable allocator to handle the case where the input and output are aliased, that is they refer to the same underlying object. The change to the allocator itself is relatively small, but accompanied with an extended suite of property-based allocation tests run on randomly generated programs: * every operand has a name * names used are exactly: input, output, temporaries * input not written to * input and output not live at same time (required for aliasing) * live operands have unique names * executing the program gives the right result To support these tests two new packages were added: * acc/eval: interpreter for acc programs based on variable names * acc/rand: random generator for acc programs As a final integration test, the fp25519 example now contains a test for aliased execution. Updates #129
1 parent 2cfd897 commit 35fc856

File tree

13 files changed

+732
-244
lines changed

13 files changed

+732
-244
lines changed

acc/eval/interp.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Package eval provides an interpreter for acc programs.
2+
package eval
3+
4+
import (
5+
"fmt"
6+
"math/big"
7+
8+
"github.com/mmcloughlin/addchain/acc/ir"
9+
"github.com/mmcloughlin/addchain/internal/errutil"
10+
)
11+
12+
// Interpreter for acc programs. In contrast to evaluation using indexes, the
13+
// interpreter executes the program using operand variable names, as if it was a
14+
// block of source code. Internally it maintains the state of every variable,
15+
// and program instructions update that state.
16+
type Interpreter struct {
17+
state map[string]*big.Int
18+
}
19+
20+
// NewInterpreter builds a new interpreter. Initially, all variables are unset.
21+
func NewInterpreter() *Interpreter {
22+
return &Interpreter{
23+
state: map[string]*big.Int{},
24+
}
25+
}
26+
27+
// Load the named variable.
28+
func (i *Interpreter) Load(v string) (*big.Int, bool) {
29+
x, ok := i.state[v]
30+
return x, ok
31+
}
32+
33+
// Store x into the named variable.
34+
func (i *Interpreter) Store(v string, x *big.Int) {
35+
i.state[v] = x
36+
}
37+
38+
// Initialize the variable v to x. Errors if v is already defined.
39+
func (i *Interpreter) Initialize(v string, x *big.Int) error {
40+
if _, ok := i.Load(v); ok {
41+
return fmt.Errorf("variable %q is already defined", v)
42+
}
43+
i.Store(v, x)
44+
return nil
45+
}
46+
47+
// Execute the program p.
48+
func (i *Interpreter) Execute(p *ir.Program) error {
49+
for _, inst := range p.Instructions {
50+
if err := i.instruction(inst); err != nil {
51+
return err
52+
}
53+
}
54+
return nil
55+
}
56+
57+
func (i *Interpreter) instruction(inst *ir.Instruction) error {
58+
output := i.output(inst.Output)
59+
switch op := inst.Op.(type) {
60+
case ir.Add:
61+
x, err := i.operands(op.X, op.Y)
62+
if err != nil {
63+
return err
64+
}
65+
output.Add(x[0], x[1])
66+
case ir.Double:
67+
x, err := i.operand(op.X)
68+
if err != nil {
69+
return err
70+
}
71+
output.Add(x, x)
72+
case ir.Shift:
73+
x, err := i.operand(op.X)
74+
if err != nil {
75+
return err
76+
}
77+
output.Lsh(x, op.S)
78+
default:
79+
return errutil.UnexpectedType(op)
80+
}
81+
return nil
82+
}
83+
84+
func (i *Interpreter) output(operand *ir.Operand) *big.Int {
85+
if x, ok := i.Load(operand.Identifier); ok {
86+
return x
87+
}
88+
x := new(big.Int)
89+
i.Store(operand.Identifier, x)
90+
return x
91+
}
92+
93+
func (i *Interpreter) operands(operands ...*ir.Operand) ([]*big.Int, error) {
94+
xs := make([]*big.Int, 0, len(operands))
95+
for _, operand := range operands {
96+
x, err := i.operand(operand)
97+
if err != nil {
98+
return nil, err
99+
}
100+
xs = append(xs, x)
101+
}
102+
return xs, nil
103+
}
104+
105+
func (i *Interpreter) operand(operand *ir.Operand) (*big.Int, error) {
106+
if operand.Identifier == "" {
107+
return nil, fmt.Errorf("operand %s missing identifier", operand)
108+
}
109+
x, ok := i.Load(operand.Identifier)
110+
if !ok {
111+
return nil, fmt.Errorf("operand %q is not defined", operand)
112+
}
113+
return x, nil
114+
}

acc/eval/interp_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package eval
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/mmcloughlin/addchain/acc/ir"
8+
"github.com/mmcloughlin/addchain/internal/bigint"
9+
)
10+
11+
func TestInterpreter(t *testing.T) {
12+
// Construct a test input program with named operands. The result should be
13+
// 0b1111.
14+
a := ir.NewOperand("a", 0)
15+
b := ir.NewOperand("b", 1)
16+
c := ir.NewOperand("c", 2)
17+
d := ir.NewOperand("d", 4)
18+
e := ir.NewOperand("e", 5)
19+
p := &ir.Program{
20+
Instructions: []*ir.Instruction{
21+
{Output: b, Op: ir.Double{X: a}},
22+
{Output: c, Op: ir.Add{X: a, Y: b}},
23+
{Output: d, Op: ir.Shift{X: c, S: 2}},
24+
{Output: e, Op: ir.Add{X: d, Y: c}},
25+
},
26+
}
27+
28+
t.Logf("program:\n%s", p)
29+
30+
// Evaluate it.
31+
i := NewInterpreter()
32+
33+
if err := i.Initialize("a", big.NewInt(1)); err != nil {
34+
t.Fatal(err)
35+
}
36+
37+
if err := i.Execute(p); err != nil {
38+
t.Fatal(err)
39+
}
40+
41+
// Check variable values.
42+
expect := map[string]int64{
43+
"a": 1,
44+
"b": 2,
45+
"c": 3,
46+
"d": 12,
47+
"e": 15,
48+
}
49+
for v, x := range expect {
50+
got, ok := i.Load(v)
51+
if !ok {
52+
t.Fatalf("missing value for %q", v)
53+
}
54+
if !bigint.EqualInt64(got, x) {
55+
t.Errorf("got %s=%v; expect %v", v, got, x)
56+
}
57+
}
58+
}

acc/ir/ir.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Program struct {
1313
Instructions []*Instruction
1414

1515
// Pass/analysis results.
16+
Indexes []int
1617
Operands map[int]*Operand
1718
ReadCount map[int]int
1819
Program addchain.Program

acc/pass/alloc.go

+44-30
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,22 @@ type Allocator struct {
2626

2727
// Execute performs temporary variable allocation.
2828
func (a Allocator) Execute(p *ir.Program) error {
29-
// Canonicalize operands and delete all names.
30-
if err := Exec(p, Func(CanonicalizeOperands), Func(ClearNames)); err != nil {
29+
// Canonicalize operands, collect unique indexes, and delete all names.
30+
if err := Exec(p, Func(CanonicalizeOperands), Func(Indexes), Func(ClearNames)); err != nil {
3131
return err
3232
}
3333

34-
// Initialize allocation. This maps operand index to variable index. The
35-
// inidicies 0 and 1 are special, reserved for the input and output
36-
// respectively. Any indicies above that are temporaries.
37-
out := p.Output()
38-
allocation := map[int]int{
39-
0: 0,
40-
out.Index: 1,
41-
}
42-
n := 2
34+
// Keep an allocation map from operand index to variable index.
35+
allocation := map[int]int{}
4336

44-
// Keep a heap of available indicies. Initially none.
37+
// Keep a heap of available variables, and a total variable count.
4538
available := heap.NewMinInts()
39+
n := 0
40+
41+
// Assign a variable for the output.
42+
out := p.Output()
43+
allocation[out.Index] = 0
44+
n = 1
4645

4746
// Process instructions in reverse.
4847
for i := len(p.Instructions) - 1; i >= 0; i-- {
@@ -72,27 +71,42 @@ func (a Allocator) Execute(p *ir.Program) error {
7271
}
7372
}
7473

75-
// Record allocation.
76-
for _, op := range p.Operands {
77-
op.Identifier = a.name(allocation[op.Index])
74+
// Assign names to the operands. Reuse of the output variable is handled
75+
// specially, since we have to account for the possibility that it could be
76+
// aliased with the input. Prior to the last use of the input, variable 0
77+
// will be a temporary, after it will be the output.
78+
lastinputread := 0
79+
for _, inst := range p.Instructions {
80+
for _, input := range inst.Op.Inputs() {
81+
if input.Index == 0 {
82+
lastinputread = inst.Output.Index
83+
}
84+
}
7885
}
7986

80-
temps := []string{}
81-
for i := 2; i < n; i++ {
82-
temps = append(temps, a.name(i))
87+
// Map from variable index to name.
88+
name := map[int]string{}
89+
for _, index := range p.Indexes {
90+
op := p.Operands[index]
91+
v := allocation[op.Index]
92+
_, ok := name[v]
93+
switch {
94+
// Operand index 0 is the input.
95+
case op.Index == 0:
96+
op.Identifier = a.Input
97+
// Variable index 0 is the output, as long as we're past the last use of
98+
// the input.
99+
case v == 0 && op.Index >= lastinputread:
100+
op.Identifier = a.Output
101+
// Unnamed variable: allocate a temporary.
102+
case !ok:
103+
name[v] = fmt.Sprintf(a.Format, len(p.Temporaries))
104+
p.Temporaries = append(p.Temporaries, name[v])
105+
fallthrough
106+
default:
107+
op.Identifier = name[v]
108+
}
83109
}
84-
p.Temporaries = temps
85110

86111
return nil
87112
}
88-
89-
func (a Allocator) name(v int) string {
90-
switch v {
91-
case 0:
92-
return a.Input
93-
case 1:
94-
return a.Output
95-
default:
96-
return fmt.Sprintf(a.Format, v-2)
97-
}
98-
}

0 commit comments

Comments
 (0)