Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

acc: allocation resilient to aliased input/output #134

Merged
merged 16 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions acc/eval/interp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Package eval provides an interpreter for acc programs.
package eval

import (
"fmt"
"math/big"

"github.com/mmcloughlin/addchain/acc/ir"
"github.com/mmcloughlin/addchain/internal/errutil"
)

// Interpreter for acc programs. In contrast to evaluation using indexes, the
// interpreter executes the program using operand variable names, as if it was a
// block of source code. Internally it maintains the state of every variable,
// and program instructions update that state.
type Interpreter struct {
state map[string]*big.Int
}

// NewInterpreter builds a new interpreter. Initially, all variables are unset.
func NewInterpreter() *Interpreter {
return &Interpreter{
state: map[string]*big.Int{},
}
}

// Load the named variable.
func (i *Interpreter) Load(v string) (*big.Int, bool) {
x, ok := i.state[v]
return x, ok
}

// Store x into the named variable.
func (i *Interpreter) Store(v string, x *big.Int) {
i.state[v] = x
}

// Initialize the variable v to x. Errors if v is already defined.
func (i *Interpreter) Initialize(v string, x *big.Int) error {
if _, ok := i.Load(v); ok {
return fmt.Errorf("variable %q is already defined", v)
}
i.Store(v, x)
return nil
}

// Execute the program p.
func (i *Interpreter) Execute(p *ir.Program) error {
for _, inst := range p.Instructions {
if err := i.instruction(inst); err != nil {
return err
}
}
return nil
}

func (i *Interpreter) instruction(inst *ir.Instruction) error {
output := i.output(inst.Output)
switch op := inst.Op.(type) {
case ir.Add:
x, err := i.operands(op.X, op.Y)
if err != nil {
return err
}
output.Add(x[0], x[1])
case ir.Double:
x, err := i.operand(op.X)
if err != nil {
return err
}
output.Add(x, x)
case ir.Shift:
x, err := i.operand(op.X)
if err != nil {
return err
}
output.Lsh(x, op.S)
default:
return errutil.UnexpectedType(op)
}
return nil
}

func (i *Interpreter) output(operand *ir.Operand) *big.Int {
if x, ok := i.Load(operand.Identifier); ok {
return x
}
x := new(big.Int)
i.Store(operand.Identifier, x)
return x
}

func (i *Interpreter) operands(operands ...*ir.Operand) ([]*big.Int, error) {
xs := make([]*big.Int, 0, len(operands))
for _, operand := range operands {
x, err := i.operand(operand)
if err != nil {
return nil, err
}
xs = append(xs, x)
}
return xs, nil
}

func (i *Interpreter) operand(operand *ir.Operand) (*big.Int, error) {
if operand.Identifier == "" {
return nil, fmt.Errorf("operand %s missing identifier", operand)
}
x, ok := i.Load(operand.Identifier)
if !ok {
return nil, fmt.Errorf("operand %q is not defined", operand)
}
return x, nil
}
58 changes: 58 additions & 0 deletions acc/eval/interp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package eval

import (
"math/big"
"testing"

"github.com/mmcloughlin/addchain/acc/ir"
"github.com/mmcloughlin/addchain/internal/bigint"
)

func TestInterpreter(t *testing.T) {
// Construct a test input program with named operands. The result should be
// 0b1111.
a := ir.NewOperand("a", 0)
b := ir.NewOperand("b", 1)
c := ir.NewOperand("c", 2)
d := ir.NewOperand("d", 4)
e := ir.NewOperand("e", 5)
p := &ir.Program{
Instructions: []*ir.Instruction{
{Output: b, Op: ir.Double{X: a}},
{Output: c, Op: ir.Add{X: a, Y: b}},
{Output: d, Op: ir.Shift{X: c, S: 2}},
{Output: e, Op: ir.Add{X: d, Y: c}},
},
}

t.Logf("program:\n%s", p)

// Evaluate it.
i := NewInterpreter()

if err := i.Initialize("a", big.NewInt(1)); err != nil {
t.Fatal(err)
}

if err := i.Execute(p); err != nil {
t.Fatal(err)
}

// Check variable values.
expect := map[string]int64{
"a": 1,
"b": 2,
"c": 3,
"d": 12,
"e": 15,
}
for v, x := range expect {
got, ok := i.Load(v)
if !ok {
t.Fatalf("missing value for %q", v)
}
if !bigint.EqualInt64(got, x) {
t.Errorf("got %s=%v; expect %v", v, got, x)
}
}
}
1 change: 1 addition & 0 deletions acc/ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Program struct {
Instructions []*Instruction

// Pass/analysis results.
Indexes []int
Operands map[int]*Operand
ReadCount map[int]int
Program addchain.Program
Expand Down
74 changes: 44 additions & 30 deletions acc/pass/alloc.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,22 @@ type Allocator struct {

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

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

// Keep a heap of available indicies. Initially none.
// Keep a heap of available variables, and a total variable count.
available := heap.NewMinInts()
n := 0

// Assign a variable for the output.
out := p.Output()
allocation[out.Index] = 0
n = 1

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

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

temps := []string{}
for i := 2; i < n; i++ {
temps = append(temps, a.name(i))
// Map from variable index to name.
name := map[int]string{}
for _, index := range p.Indexes {
op := p.Operands[index]
v := allocation[op.Index]
_, ok := name[v]
switch {
// Operand index 0 is the input.
case op.Index == 0:
op.Identifier = a.Input
// Variable index 0 is the output, as long as we're past the last use of
// the input.
case v == 0 && op.Index >= lastinputread:
op.Identifier = a.Output
// Unnamed variable: allocate a temporary.
case !ok:
name[v] = fmt.Sprintf(a.Format, len(p.Temporaries))
p.Temporaries = append(p.Temporaries, name[v])
fallthrough
default:
op.Identifier = name[v]
}
}
p.Temporaries = temps

return nil
}

func (a Allocator) name(v int) string {
switch v {
case 0:
return a.Input
case 1:
return a.Output
default:
return fmt.Sprintf(a.Format, v-2)
}
}
Loading