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

AVM: Switch opcode #4458

Merged
merged 23 commits into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bdc77fe
typo fix
algoidurovic Aug 19, 2022
5683c57
initial implementation
algoidurovic Aug 24, 2022
80e1b12
documentation
algoidurovic Aug 29, 2022
8ebb577
respond to feedback
algoidurovic Sep 7, 2022
011495d
fix errors
algoidurovic Sep 8, 2022
2ac7890
add tests
algoidurovic Sep 8, 2022
e35f14a
assemble test
algoidurovic Sep 8, 2022
5fc4a6a
resolve merge conflict
algoidurovic Sep 8, 2022
bf2c849
fix doc formatting and failing test
algoidurovic Sep 9, 2022
7e6014f
run make for doc generation, and extract offset decoding into dedicat…
algoidurovic Sep 9, 2022
b9b417a
touch up
algoidurovic Sep 9, 2022
933df40
update tests
algoidurovic Sep 9, 2022
9d1f0de
Allow fall-through in switch
jannotti Sep 15, 2022
c0fa16c
Replace hard-coded version with AssemblerMaxVersion in assembler_test.go
michaeldiamant Sep 15, 2022
76b87f5
Update docs for switchi fall-through behavior
michaeldiamant Sep 15, 2022
d78965e
Add switchi assembly test case
michaeldiamant Sep 15, 2022
b1aa647
Merge pull request #12 from michaeldiamant/switch_fallthrough_pr_review
jannotti Sep 15, 2022
c066ff2
Changing encoding to use a single byte for label count
jannotti Sep 15, 2022
d705b0c
Fix switch disassembly
jannotti Sep 15, 2022
928966a
docVersion and bump pairing ops
jannotti Sep 16, 2022
e88d580
Merge pull request #3 from jannotti/switch-fallthrough
jannotti Sep 16, 2022
96fe7c1
change name to switch. we've agreed next one would be `match`
jannotti Sep 16, 2022
f103b5e
Merge pull request #5 from jannotti/switch-name
jannotti Sep 16, 2022
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
97 changes: 81 additions & 16 deletions data/transactions/logic/assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ type Writer interface {
type labelReference struct {
sourceLine int

// position of the opcode start that refers to the label
// position of the label reference
position int

label string

// ending positions of the opcode containing the label reference.
offsetPosition int
}

type constReference interface {
Expand Down Expand Up @@ -346,8 +349,8 @@ func (ops *OpStream) recordSourceLine() {
}

// referToLabel records an opcode label reference to resolve later
func (ops *OpStream) referToLabel(pc int, label string) {
ops.labelReferences = append(ops.labelReferences, labelReference{ops.sourceLine, pc, label})
func (ops *OpStream) referToLabel(pc int, label string, offsetPosition int) {
ops.labelReferences = append(ops.labelReferences, labelReference{ops.sourceLine, pc, label, offsetPosition})
}

type refineFunc func(pgm *ProgramKnowledge, immediates []string) (StackTypes, StackTypes)
Expand Down Expand Up @@ -510,7 +513,7 @@ func asmInt(ops *OpStream, spec *OpSpec, args []string) error {
ops.IntLiteral(i)
return nil
}
// check OnCompetion constants
// check OnCompletion constants
oc, isOCStr := onCompletionMap[args[0]]
if isOCStr {
ops.IntLiteral(oc)
Expand Down Expand Up @@ -903,14 +906,30 @@ func asmBranch(ops *OpStream, spec *OpSpec, args []string) error {
return ops.error("branch operation needs label argument")
}

ops.referToLabel(ops.pending.Len(), args[0])
ops.referToLabel(ops.pending.Len()+1, args[0], ops.pending.Len()+spec.Size)
ops.pending.WriteByte(spec.Opcode)
// zero bytes will get replaced with actual offset in resolveLabels()
ops.pending.WriteByte(0)
ops.pending.WriteByte(0)
return nil
}

func asmSwitch(ops *OpStream, spec *OpSpec, args []string) error {
numOffsets := uint64(len(args))
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)
for _, arg := range args {
ops.referToLabel(ops.pending.Len(), arg, opEndPos)
// zero bytes will get replaced with actual offset in resolveLabels()
ops.pending.WriteByte(0)
ops.pending.WriteByte(0)
}
return nil
}

func asmSubstring(ops *OpStream, spec *OpSpec, args []string) error {
err := asmDefault(ops, spec, args)
if err != nil {
Expand Down Expand Up @@ -1815,19 +1834,20 @@ func (ops *OpStream) resolveLabels() {
reported[lr.label] = true
continue
}
// all branch instructions (currently) are opcode byte and 2 offset bytes, and the destination is relative to the next pc as if the branch was a no-op
naturalPc := lr.position + 3
if ops.Version < backBranchEnabledVersion && dest < naturalPc {

jannotti marked this conversation as resolved.
Show resolved Hide resolved
// All branch targets are encoded as 2 offset bytes. The destination is relative to the end of the
// instruction they appear in, which is available in lr.offsetPostion
if ops.Version < backBranchEnabledVersion && dest < lr.offsetPosition {
ops.errorf("label %#v is a back reference, back jump support was introduced in v4", lr.label)
continue
}
jump := dest - naturalPc
jump := dest - lr.offsetPosition
if jump > 0x7fff {
ops.errorf("label %#v is too far away", lr.label)
continue
}
raw[lr.position+1] = uint8(jump >> 8)
raw[lr.position+2] = uint8(jump & 0x0ff)
raw[lr.position] = uint8(jump >> 8)
raw[lr.position+1] = uint8(jump & 0x0ff)
}
ops.pending = *bytes.NewBuffer(raw)
ops.sourceLine = saved
Expand Down Expand Up @@ -2061,6 +2081,7 @@ func (ops *OpStream) optimizeConstants(refs []constReference, constBlock []inter
for i := range ops.labelReferences {
if ops.labelReferences[i].position > position {
ops.labelReferences[i].position += positionDelta
ops.labelReferences[i].offsetPosition += positionDelta
}
}

Expand Down Expand Up @@ -2297,11 +2318,8 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) {

pc++
case immLabel:
offset := (uint(dis.program[pc]) << 8) | uint(dis.program[pc+1])
target := int(offset) + pc + 2
if target > 0xffff {
target -= 0x10000
}
offset := decodeBranchOffset(dis.program, pc)
target := offset + pc + 2
var label string
if dis.numericTargets {
label = fmt.Sprintf("%d", target)
Expand Down Expand Up @@ -2363,6 +2381,30 @@ func disassemble(dis *disassembleState, spec *OpSpec) (string, error) {
out += fmt.Sprintf("0x%s", hex.EncodeToString(bv))
}
pc = nextpc
case immLabels:
targets, nextpc, err := parseSwitch(dis.program, pc)
if err != nil {
return "", err
}

var labels []string
for _, target := range targets {
var label string
if dis.numericTargets {
label = fmt.Sprintf("%d", target)
} else {
if known, ok := dis.pendingLabels[target]; ok {
label = known
} else {
dis.labelCount++
label = fmt.Sprintf("label%d", dis.labelCount)
dis.putLabel(label, target)
}
}
labels = append(labels, label)
}
out += strings.Join(labels, " ")
pc = nextpc
default:
return "", fmt.Errorf("unknown immKind %d", imm.kind)
}
Expand Down Expand Up @@ -2516,6 +2558,29 @@ func checkByteConstBlock(cx *EvalContext) error {
return nil
}

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++ {
offset := decodeBranchOffset(program, pos)
target := int(offset) + int(end)
targets = append(targets, target)
pos += 2
}
nextpc = pos
return
}

func allPrintableASCII(bytes []byte) bool {
for _, b := range bytes {
if b < 32 || b > 126 {
Expand Down
66 changes: 64 additions & 2 deletions data/transactions/logic/assembler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package logic

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

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

const v6Compiled = "2004010002b7a60c26050242420c68656c6c6f20776f726c6421070123456789abcd208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d047465737400320032013202320380021234292929292b0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e01022581f8acd19181cf959a1281f8acd19181cf951a81f8acd19181cf1581f8acd191810f082209240a220b230c240d250e230f23102311231223132314181b1c28171615400003290349483403350222231d4a484848482b50512a632223524100034200004322602261222704634848222862482864286548482228246628226723286828692322700048482371004848361c0037001a0031183119311b311d311e311f312023221e312131223123312431253126312731283129312a312b312c312d312e312f447825225314225427042455220824564c4d4b0222382124391c0081e80780046a6f686e2281d00f23241f880003420001892224902291922494249593a0a1a2a3a4a5a6a7a8a9aaabacadae24af3a00003b003c003d816472064e014f012a57000823810858235b235a2359b03139330039b1b200b322c01a23c1001a2323c21a23c3233e233f8120af06002a494905002a49490700b53a03b6b7043cb8033a0c2349c42a9631007300810881088120978101c53a8101c6003a"

Expand All @@ -403,7 +410,7 @@ const randomnessCompiled = "81ffff03d101d000"
const v7Compiled = v6Compiled + "5e005f018120af060180070123456789abcd49490501988003012345494984" +
randomnessCompiled + "800243218001775c0280018881015d"

const v8Compiled = v7Compiled + pairingCompiled
const v8Compiled = v7Compiled + pairingCompiled + "8101e002fff800008101"

var nonsense = map[uint64]string{
1: v1Nonsense,
Expand Down Expand Up @@ -2759,3 +2766,58 @@ func TestSemiColon(t *testing.T) {
`byte "test;this";;;pop;`,
)
}

func TestAssembleSwitch(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

// fail when target doesn't correspond to existing label
source := `
pushint 1
switchi label1 label2
label1:
`
testProg(t, source, 8, NewExpect(3, "reference to undefined label \"label2\""))

// confirm size of varuint list size
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))
}

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

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)

// allow duplicate label reference
source = `
pushint 1
switchi label1 label1
label1:
`
testProg(t, source, 8)
}
7 changes: 6 additions & 1 deletion data/transactions/logic/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ var opDocByName = map[string]string{

"vrf_verify": "Verify the proof B of message A against pubkey C. Returns vrf output and verification flag.",
"block": "field F of block A. Fail unless A falls between txn.LastValid-1002 and txn.FirstValid (exclusive)",

"switchi": "branch to target at index A. Fail if index A is out of bounds.",
}

// OpDoc returns a description of the op
Expand Down Expand Up @@ -261,6 +263,8 @@ var opcodeImmediateNotes = map[string]string{

"vrf_verify": "{uint8 parameters index}",
"block": "{uint8 block field}",

"switchi": "{varuint length} [{int16 branch offset, big-endian}, ...]",
}

// OpImmediateNote returns a short string about immediate data which follows the op byte
Expand Down Expand Up @@ -323,6 +327,7 @@ var opDocExtras = map[string]string{
"itxn_submit": "`itxn_submit` resets the current transaction so that it can not be resubmitted. A new `itxn_begin` is required to prepare another inner transaction.",
"base64_decode": "*Warning*: Usage should be restricted to very rare use cases. In almost all cases, smart contracts should directly handle non-encoded byte-strings. This opcode should only be used in cases where base64 is the only available option, e.g. interoperability with a third-party that only signs base64 strings.\n\n Decodes A using the base64 encoding E. Specify the encoding with an immediate arg either as URL and Filename Safe (`URLEncoding`) or Standard (`StdEncoding`). See [RFC 4648 sections 4 and 5](https://rfc-editor.org/rfc/rfc4648.html#section-4). It is assumed that the encoding ends with the exact number of `=` padding characters as required by the RFC. When padding occurs, any unused pad bits in the encoding must be set to zero or the decoding will fail. The special cases of `\\n` and `\\r` are allowed but completely ignored. An error will result when attempting to decode a string with a character that is not in the encoding alphabet or not one of `=`, `\\r`, or `\\n`.",
"json_ref": "*Warning*: Usage should be restricted to very rare use cases, as JSON decoding is expensive and quite limited. In addition, JSON objects are large and not optimized for size.\n\nAlmost all smart contracts should use simpler and smaller methods (such as the [ABI](https://arc.algorand.foundation/ARCs/arc-0004). This opcode should only be used in cases where JSON is only available option, e.g. when a third-party only signs JSON.",
"switchi": "The `switchi` instruction opcode 0xe0 is followed by `n`, the number of targets, each of which are encoded as 2 byte values indicating the position of the target label relative to the end of the `switchi` instruction (i.e. the offset). The last element on the stack represents the index of the target to branch to. If the index is greater than or equal to n, the evaluation will fail. Otherwise, the program will branch to `pc + 1 + sizeof(n) + 2 * n + target[index]`. Branch targets must be aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.)",
}

// OpDocExtra returns extra documentation text about an op
Expand All @@ -339,7 +344,7 @@ var OpGroups = map[string][]string{
"Byte Array Arithmetic": {"b+", "b-", "b/", "b*", "b<", "b>", "b<=", "b>=", "b==", "b!=", "b%", "bsqrt"},
"Byte Array Logic": {"b|", "b&", "b^", "b~"},
"Loading Values": {"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "pushint", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "pushbytes", "bzero", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "args", "txn", "gtxn", "txna", "txnas", "gtxna", "gtxnas", "gtxns", "gtxnsa", "gtxnsas", "global", "load", "loads", "store", "stores", "gload", "gloads", "gloadss", "gaid", "gaids"},
"Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "retsub"},
"Flow Control": {"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2", "dig", "cover", "uncover", "swap", "select", "assert", "callsub", "retsub", "switchi"},
"State Access": {"balance", "min_balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get", "app_params_get", "acct_params_get", "log", "block"},
"Inner Transactions": {"itxn_begin", "itxn_next", "itxn_field", "itxn_submit", "itxn", "itxna", "itxnas", "gitxn", "gitxna", "gitxnas"},
}
Expand Down
Loading