Skip to content

Commit

Permalink
Merge pull request #3 from jannotti/switch-fallthrough
Browse files Browse the repository at this point in the history
Allow fall-through in switch
  • Loading branch information
jannotti authored Sep 16, 2022
2 parents 933df40 + 928966a commit e88d580
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 128 deletions.
2 changes: 1 addition & 1 deletion cmd/opdoc/opdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"github.com/algorand/go-algorand/protocol"
)

var docVersion = 7
var docVersion = 8

func opGroupMarkdownTable(names []string, out io.Writer) {
fmt.Fprint(out, `| Opcode | Description |
Expand Down
1 change: 1 addition & 0 deletions data/transactions/logic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ Account fields used in the `acct_params_get` opcode.
| `assert` | immediately fail unless A is a non-zero number |
| `callsub target` | branch unconditionally to TARGET, saving the next instruction on the call stack |
| `retsub` | pop the top instruction from the call stack and branch to it |
| `switchi target ...` | branch to the Ath label. Continue at following instruction if index A exceeds the number of labels. |

### State Access

Expand Down
7 changes: 7 additions & 0 deletions data/transactions/logic/TEAL_opcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,13 @@ The call stack is separate from the data stack. Only `callsub` and `retsub` mani

The call stack is separate from the data stack. Only `callsub` and `retsub` manipulate it.

## switchi target ...

- Opcode: 0x8a {uint8 branch count} [{int16 branch offset, big-endian}, ...]
- Stack: ..., A: uint64 → ...
- branch to the Ath label. Continue at following instruction if index A exceeds the number of labels.
- Availability: v8

## shl

- Opcode: 0x90
Expand Down
31 changes: 12 additions & 19 deletions data/transactions/logic/assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"errors"
"fmt"
"io"
"math"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -915,12 +916,13 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error {
}

func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error {
numOffsets := uint64(len(args))
numOffsets := len(args)
if numOffsets > math.MaxUint8 {
return ops.errorf("%s cannot take more than 255 labels", spec.Name)
}
ops.pending.WriteByte(spec.Opcode)
var scratch [binary.MaxVarintLen64]byte
vlen := binary.PutUvarint(scratch[:], uint64(len(args)))
ops.pending.Write(scratch[:vlen])
opEndPos := ops.pending.Len() + 2*int(numOffsets)
ops.pending.WriteByte(byte(numOffsets))
opEndPos := ops.pending.Len() + 2*numOffsets
for _, arg := range args {
ops.referToLabel(ops.pending.Len(), arg, opEndPos)
// zero bytes will get replaced with actual offset in resolveLabels()
Expand Down Expand Up @@ -2559,21 +2561,12 @@ func checkByteConstBlock(cx *EvalContext) error {
}

func parseSwitch(program []byte, pos int) (targets []int, nextpc int, err error) {
numOffsets, bytesUsed := binary.Uvarint(program[pos:])
if bytesUsed <= 0 {
err = fmt.Errorf("could not decode switch target list size at pc=%d", pos)
return
}
pos += bytesUsed
if numOffsets > uint64(len(program)) {
err = errTooManyItems
return
}

end := pos + int(2*numOffsets) // end of op: offset is applied to this position
for i := 0; i < int(numOffsets); i++ {
numOffsets := int(program[pos])
pos++
end := pos + 2*numOffsets // end of op: offset is applied to this position
for i := 0; i < numOffsets; i++ {
offset := decodeBranchOffset(program, pos)
target := int(offset) + int(end)
target := end + offset
targets = append(targets, target)
pos += 2
}
Expand Down
136 changes: 74 additions & 62 deletions data/transactions/logic/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package logic

import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"strings"
Expand Down Expand Up @@ -395,22 +394,30 @@ pushint 1
replace3
`

const v8Nonsense = v7Nonsense + pairingNonsense + `
const switchNonsense = `
switch_label0:
pushint 1
switchi switch_label0 switch_label1
switch_label1:
pushint 1
`

const v8Nonsense = v7Nonsense + switchNonsense

const v9Nonsense = v8Nonsense + pairingNonsense

const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a"

const randomnessCompiled = "81ffff03d101d000"

const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" +
randomnessCompiled + "800243218001775c0280018881015d"

const v8Compiled = v7Compiled + pairingCompiled + "8101e002fff800008101"
const switchCompiled = "81018a02fff800008101"

const v8Compiled = v7Compiled + switchCompiled

const v9Compiled = v7Compiled + pairingCompiled

var nonsense = map[uint64]string{
1: v1Nonsense,
Expand All @@ -421,6 +428,7 @@ var nonsense = map[uint64]string{
6: v6Nonsense,
7: v7Nonsense,
8: v8Nonsense,
9: v9Nonsense,
}

var compiled = map[uint64]string{
Expand Down Expand Up @@ -517,16 +525,21 @@ type Expect struct {
s string
}

func testMatch(t testing.TB, actual, expected string) bool {
func testMatch(t testing.TB, actual, expected string) (ok bool) {
defer func() {
if !ok {
t.Logf("'%s' does not match '%s'", actual, expected)
}
}()
t.Helper()
if strings.HasPrefix(expected, "...") && strings.HasSuffix(expected, "...") {
return assert.Contains(t, actual, expected[3:len(expected)-3])
return strings.Contains(actual, expected[3:len(expected)-3])
} else if strings.HasPrefix(expected, "...") {
return assert.Contains(t, actual+"^", expected[3:]+"^")
return strings.Contains(actual+"^", expected[3:]+"^")
} else if strings.HasSuffix(expected, "...") {
return assert.Contains(t, "^"+actual, "^"+expected[:len(expected)-3])
return strings.Contains("^"+actual, "^"+expected[:len(expected)-3])
} else {
return assert.Equal(t, expected, actual)
return expected == actual
}
}

Expand Down Expand Up @@ -597,13 +610,13 @@ func testProg(t testing.TB, source string, ver uint64, expected ...Expect) *OpSt
errors := ops.Errors
for _, exp := range expected {
if exp.l == 0 {
// line 0 means: "must match all"
// line 0 means: "must match some line"
require.Len(t, expected, 1)
fail := false
fail := true
for _, err := range errors {
msg := err.Unwrap().Error()
if !testMatch(t, msg, exp.s) {
fail = true
if testMatch(t, msg, exp.s) {
fail = false
}
}
if fail {
Expand Down Expand Up @@ -1966,8 +1979,7 @@ intc_0 // 1
bnz label1
label1:
`, v)
ops, err := AssembleStringWithVersion(source, v)
require.NoError(t, err)
ops := testProg(t, source, v)
dis, err := Disassemble(ops.Program)
require.NoError(t, err)
require.Equal(t, source, dis)
Expand Down Expand Up @@ -2080,8 +2092,7 @@ func TestHasStatefulOps(t *testing.T) {
t.Parallel()

source := "int 1"
ops, err := AssembleStringWithVersion(source, AssemblerMaxVersion)
require.NoError(t, err)
ops := testProg(t, source, AssemblerMaxVersion)
has, err := HasStatefulOps(ops.Program)
require.NoError(t, err)
require.False(t, has)
Expand All @@ -2091,8 +2102,7 @@ int 1
app_opted_in
err
`
ops, err = AssembleStringWithVersion(source, AssemblerMaxVersion)
require.NoError(t, err)
ops = testProg(t, source, AssemblerMaxVersion)
has, err = HasStatefulOps(ops.Program)
require.NoError(t, err)
require.True(t, has)
Expand Down Expand Up @@ -2269,46 +2279,38 @@ func TestAssemblePragmaVersion(t *testing.T) {
text := `#pragma version 1
int 1
`
ops, err := AssembleStringWithVersion(text, 1)
require.NoError(t, err)
ops1, err := AssembleStringWithVersion("int 1", 1)
require.NoError(t, err)
ops := testProg(t, text, 1)
ops1 := testProg(t, "int 1", 1)
require.Equal(t, ops1.Program, ops.Program)

testProg(t, text, 0, Expect{1, "version mismatch..."})
testProg(t, text, 2, Expect{1, "version mismatch..."})
testProg(t, text, assemblerNoVersion)

ops, err = AssembleStringWithVersion(text, assemblerNoVersion)
require.NoError(t, err)
ops = testProg(t, text, assemblerNoVersion)
require.Equal(t, ops1.Program, ops.Program)

text = `#pragma version 2
int 1
`
ops, err = AssembleStringWithVersion(text, 2)
require.NoError(t, err)
ops2, err := AssembleStringWithVersion("int 1", 2)
require.NoError(t, err)
ops = testProg(t, text, 2)
ops2 := testProg(t, "int 1", 2)
require.Equal(t, ops2.Program, ops.Program)

testProg(t, text, 0, Expect{1, "version mismatch..."})
testProg(t, text, 1, Expect{1, "version mismatch..."})

ops, err = AssembleStringWithVersion(text, assemblerNoVersion)
require.NoError(t, err)
ops = testProg(t, text, assemblerNoVersion)
require.Equal(t, ops2.Program, ops.Program)

// check if no version it defaults to v1
text = `byte "test"
len
`
ops, err = AssembleStringWithVersion(text, assemblerNoVersion)
require.NoError(t, err)
ops1, err = AssembleStringWithVersion(text, 1)
ops = testProg(t, text, assemblerNoVersion)
ops1 = testProg(t, text, 1)
require.Equal(t, ops1.Program, ops.Program)
require.NoError(t, err)
ops2, err = AssembleString(text)
ops2, err := AssembleString(text)
require.NoError(t, err)
require.Equal(t, ops2.Program, ops.Program)

Expand Down Expand Up @@ -2336,9 +2338,8 @@ func TestErrShortBytecblock(t *testing.T) {
t.Parallel()

text := `intcblock 0x1234567812345678 0x1234567812345671 0x1234567812345672 0x1234567812345673 4 5 6 7 8`
ops, err := AssembleStringWithVersion(text, 1)
require.NoError(t, err)
_, _, err = parseIntcblock(ops.Program, 1)
ops := testProg(t, text, 1)
_, _, err := parseIntcblock(ops.Program, 1)
require.Equal(t, err, errShortIntcblock)

var cx EvalContext
Expand Down Expand Up @@ -2380,8 +2381,7 @@ func TestMethodWarning(t *testing.T) {
for _, test := range tests {
for v := uint64(1); v <= AssemblerMaxVersion; v++ {
src := fmt.Sprintf("method \"%s\"\nint 1", test.method)
ops, err := AssembleStringWithVersion(src, v)
require.NoError(t, err)
ops := testProg(t, src, v)

if test.pass {
require.Len(t, ops.Warnings, 0)
Expand Down Expand Up @@ -2684,7 +2684,7 @@ func TestMergeProtos(t *testing.T) {
func TestGetSpec(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
ops, _ := AssembleStringWithVersion("int 1", AssemblerMaxVersion)
ops := testProg(t, "int 1", AssemblerMaxVersion)
ops.versionedPseudoOps["dummyPseudo"] = make(map[int]OpSpec)
ops.versionedPseudoOps["dummyPseudo"][1] = OpSpec{Name: "b:", Version: AssemblerMaxVersion, Proto: proto("b:")}
ops.versionedPseudoOps["dummyPseudo"][2] = OpSpec{Name: ":", Version: AssemblerMaxVersion}
Expand Down Expand Up @@ -2777,47 +2777,59 @@ func TestAssembleSwitch(t *testing.T) {
switchi label1 label2
label1:
`
testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\""))
testProg(t, source, AssemblerMaxVersion, NewExpect(3, "reference to undefined label \"label2\""))

// fail when target index != uint64
testProg(t, `
byte "fail"
switchi label1
labe11:
`, AssemblerMaxVersion, Expect{3, "switchi label1 arg 0 wanted type uint64..."})

// No labels is pretty degenerate, but ok, I suppose. It's just a no-op
testProg(t, `
int 0
switchi
int 1
`, AssemblerMaxVersion)

// confirm size of varuint list size
// confirm arg limit
source = `
pushint 1
switchi label1 label2
label1:
label2:
`
ops, err := AssembleStringWithVersion(source, 8)
require.NoError(t, err)
val, bytesUsed := binary.Uvarint(ops.Program[4:])
require.Equal(t, uint64(2), val)
require.Equal(t, 1, bytesUsed)

var labelReferences []string
for i := 0; i < (1 << 9); i++ {
labelReferences = append(labelReferences, fmt.Sprintf("label%d", i))
}
ops := testProg(t, source, AssemblerMaxVersion)
require.Len(t, ops.Program, 9) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*2)

var labels []string
for i := 0; i < (1 << 9); i++ {
labels = append(labels, fmt.Sprintf("label%d:", i))
for i := 0; i < 255; i++ {
labels = append(labels, fmt.Sprintf("label%d", i))
}

// test that 255 labels is ok
source = fmt.Sprintf(`
pushint 1
switchi %s
%s
`, strings.Join(labelReferences, " "), strings.Join(labels, "\n"))
ops, err = AssembleStringWithVersion(source, 8)
require.NoError(t, err)
val, bytesUsed = binary.Uvarint(ops.Program[4:])
require.Equal(t, uint64(1<<9), val)
require.Equal(t, 2, bytesUsed)
`, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n")
ops = testProg(t, source, AssemblerMaxVersion)
require.Len(t, ops.Program, 515) // ver (1) + pushint (2) + opcode (1) + length (1) + labels (2*255)

// 256 is too many
source = fmt.Sprintf(`
pushint 1
switchi %s extra
%s
`, strings.Join(labels, " "), strings.Join(labels, ":\n")+":\n")
ops = testProg(t, source, AssemblerMaxVersion, Expect{3, "switchi cannot take more than 255 labels"})

// allow duplicate label reference
source = `
pushint 1
switchi label1 label1
label1:
`
testProg(t, source, 8)
testProg(t, source, AssemblerMaxVersion)
}
Loading

0 comments on commit e88d580

Please sign in to comment.