Skip to content
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
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{
Comment thread
jannotti marked this conversation as resolved.
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