Repository: algorand/pyteal (language: python)

Issue:
ABI-compliant app routing
## Summary

Provide a way for an application to handle requests in compliance with https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#implementing-a-method

## Scope

This might look like an ABIRouter object that you can use to build up the different routes. For example:

```python
router = ABIRouter()

router.onBareAppCall(OnComplete.NoOp, handleCreate, creation=True)
router.onBareAppCall(OnComplete.OptIn, handleOptIn)
router.onBareAppCall([OnComplete.CloseOut, OnComplete.ClearState], handleCloseOut)
router.onBareAppCall([OnComplete.Update, OnComplete.Delete], handleUpdateOrDelete)

router.onMethodCall("add(uint64,uint64)uint128", handleAdd)
router.onMethodCall("multiply(uint64,uint64)uint128", handleMultiply)
router.onMethodCall("optInAndAdd(uint64,uint64)uint128", handleOptInAndAdd, OnComplete.OptIn)
router.onMethodCall("createWithAMethod()string", handleCreateWithAMethod, creation=True)

approvalAST, clearStateAST = router.buildPrograms()
```

`onBareAppCall` can be used to register a bare app call (defined in the ABI as a call with no arguments or return value). The allowed on completion actions must be specified, as well as whether the bare call can be invoked during creation or not.

`onMethodCall` can be used to register a method call. By default `OnComplete.NoOp` will be the only allowed on completion action, but others may be specified. Additionally, you can pass in a value for `creation` if this method call should be invoked during app creation or not.

Ideally the router would also unpack the arguments and pass them to the handler function, as well as take any return value from the handler function and prefix and log it appropriately. Though this might require some more thought to implement properly.

`buildPrograms` would construct ASTs for both the approval and clear programs based on the inputs to the router. If any routes can be accessed with `OnComplete.ClearState`, these routes will be added to the clear state program.
ABI JSON generator
## Problem

As part of the ABI spec a JSON object should be made available for clients to easily read and call the methods defined in the contract.  It should be possible to auto generate this JSON object given the methods defined in the PyTeal contract using the [Router](https://github.com/algorand/pyteal/pull/170).

## Solution

Add a method to the router that can output a dict representing the structure of the JSON object that adheres to the ARC4 ABI spec

## Dependencies

https://github.com/algorand/pyteal/pull/170

## Urgency

Medium urgency. To make use of the ABI features it should be trivial to generate the JSON interface description and not require manually crafting the interface.
ABI Strings
Adding Address and String that subclass other Array types with hardcoded type specs

Closes #280 

- [x] Address type def
- [x] Address Testing
- [x] String type def
- [x] String testing

Follow up PR should implement setting directly from a byte string and mayybee a slice overload for __getitem__ on string?
`ABI Router` implementation and test
# Summary

ABI Router helps create contract approval/clear program much easier.

closes #167 and #239

## Changes

- [x] `onBareAppCall` to handle bare app call
- [x] `onMethodCall` to handle method call (app arg redirect, check method selector)
- [x] App-args de-tuple and (de)serialize for feeding to registered method, and logging return
- [x] creation argument specification (check app id == 0 condition)
- [x] ABI Json generate from router
- [x] test cases


Discussion / hints:
Some minor thoughts to write up in the thread: recall in #153 that we introduced an option in `Subroutine` such that one can specify the subroutine ABI method signature.

We can retrieve the ABI method sig from registered handler in `onMethodCall`, so we don't have to write the method sig again.

What do you think?




Fix patch:
diff --git a/pyteal/__init__.py b/pyteal/__init__.py
index ae30dd312..8087c15c3 100644
--- a/pyteal/__init__.py
+++ b/pyteal/__init__.py
@@ -17,7 +17,12 @@
     TealInputError,
     TealCompileError,
 )
-from pyteal.config import MAX_GROUP_SIZE, NUM_SLOTS
+from pyteal.config import (
+    MAX_GROUP_SIZE,
+    NUM_SLOTS,
+    RETURN_HASH_PREFIX,
+    METHOD_ARG_NUM_CUTOFF,
+)
 
 # begin __all__
 __all__ = (
@@ -37,6 +42,8 @@
         "TealCompileError",
         "MAX_GROUP_SIZE",
         "NUM_SLOTS",
+        "RETURN_HASH_PREFIX",
+        "METHOD_ARG_NUM_CUTOFF",
     ]
 )
 # end __all__
diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi
index 4f1477a90..5e49e918e 100644
--- a/pyteal/__init__.pyi
+++ b/pyteal/__init__.pyi
@@ -20,7 +20,12 @@ from pyteal.errors import (
     TealInputError,
     TealCompileError,
 )
-from pyteal.config import MAX_GROUP_SIZE, NUM_SLOTS
+from pyteal.config import (
+    MAX_GROUP_SIZE,
+    NUM_SLOTS,
+    RETURN_HASH_PREFIX,
+    METHOD_ARG_NUM_CUTOFF,
+)
 
 __all__ = [
     "ABIReturnSubroutine",
@@ -38,6 +43,7 @@ __all__ = [
     "AssetHolding",
     "AssetParam",
     "Balance",
+    "BareCallActions",
     "BinaryExpr",
     "BitLen",
     "BitwiseAnd",
@@ -64,6 +70,7 @@ __all__ = [
     "BytesSqrt",
     "BytesXor",
     "BytesZero",
+    "CallConfig",
     "CompileOptions",
     "Concat",
     "Cond",
@@ -117,8 +124,10 @@ __all__ = [
     "Lt",
     "MAX_GROUP_SIZE",
     "MAX_TEAL_VERSION",
+    "METHOD_ARG_NUM_CUTOFF",
     "MIN_TEAL_VERSION",
     "MaybeValue",
+    "MethodConfig",
     "MethodSignature",
     "MinBalance",
     "Minus",
@@ -132,14 +141,17 @@ __all__ = [
     "Nonce",
     "Not",
     "OnComplete",
+    "OnCompleteAction",
     "Op",
     "OpUp",
     "OpUpMode",
     "OptimizeOptions",
     "Or",
     "Pop",
+    "RETURN_HASH_PREFIX",
     "Reject",
     "Return",
+    "Router",
     "ScratchIndex",
     "ScratchLoad",
     "ScratchSlot",
diff --git a/pyteal/ast/__init__.py b/pyteal/ast/__init__.py
index 79f3beee5..68c6fd149 100644
--- a/pyteal/ast/__init__.py
+++ b/pyteal/ast/__init__.py
@@ -125,7 +125,6 @@
 from pyteal.ast.break_ import Break
 from pyteal.ast.continue_ import Continue
 
-
 # misc
 from pyteal.ast.scratch import (
     ScratchIndex,
@@ -139,6 +138,13 @@
 from pyteal.ast.multi import MultiValue
 from pyteal.ast.opup import OpUp, OpUpMode
 from pyteal.ast.ecdsa import EcdsaCurve, EcdsaVerify, EcdsaDecompress, EcdsaRecover
+from pyteal.ast.router import (
+    Router,
+    CallConfig,
+    MethodConfig,
+    OnCompleteAction,
+    BareCallActions,
+)
 
 # abi
 import pyteal.ast.abi as abi  # noqa: I250
@@ -280,6 +286,11 @@
     "For",
     "Break",
     "Continue",
+    "Router",
+    "CallConfig",
+    "MethodConfig",
+    "OnCompleteAction",
+    "BareCallActions",
     "abi",
     "EcdsaCurve",
     "EcdsaVerify",
diff --git a/pyteal/ast/abi/__init__.py b/pyteal/ast/abi/__init__.py
index 983cf7685..c789c3556 100644
--- a/pyteal/ast/abi/__init__.py
+++ b/pyteal/ast/abi/__init__.py
@@ -49,6 +49,7 @@
     make,
     size_of,
     type_spec_from_annotation,
+    contains_type_spec,
 )
 
 __all__ = [
@@ -103,4 +104,5 @@
     "size_of",
     "algosdk_from_annotation",
     "algosdk_from_type_spec",
+    "contains_type_spec",
 ]
diff --git a/pyteal/ast/abi/method_return.py b/pyteal/ast/abi/method_return.py
index 7d7af0ed7..b8058456a 100644
--- a/pyteal/ast/abi/method_return.py
+++ b/pyteal/ast/abi/method_return.py
@@ -4,7 +4,7 @@
 from pyteal.errors import TealInputError
 from pyteal.ast import Expr, Log, Concat, Bytes
 from pyteal.ir import TealBlock, TealSimpleBlock, Op
-from pyteal.config import RETURN_METHOD_SELECTOR
+from pyteal.config import RETURN_HASH_PREFIX
 
 if TYPE_CHECKING:
     from pyteal.compiler import CompileOptions
@@ -22,9 +22,9 @@ def __teal__(self, options: "CompileOptions") -> Tuple[TealBlock, TealSimpleBloc
             raise TealInputError(
                 f"current version {options.version} is lower than log's min version {Op.log.min_version}"
             )
-        return Log(
-            Concat(Bytes("base16", RETURN_METHOD_SELECTOR), self.arg.encode())
-        ).__teal__(options)
+        return Log(Concat(Bytes(RETURN_HASH_PREFIX), self.arg.encode())).__teal__(
+            options
+        )
 
     def __str__(self) -> str:
         return f"(MethodReturn {self.arg.type_spec()})"
diff --git a/pyteal/ast/abi/string.py b/pyteal/ast/abi/string.py
index b4215d65f..92b566d95 100644
--- a/pyteal/ast/abi/string.py
+++ b/pyteal/ast/abi/string.py
@@ -1,4 +1,4 @@
-from typing import Union, TypeVar, Sequence, cast
+from typing import Union, Sequence, cast
 from collections.abc import Sequence as CollectionSequence
 
 from pyteal.ast.abi.uint import Byte
@@ -20,9 +20,6 @@ def encoded_string(s: Expr):
     return Concat(Suffix(Itob(Len(s)), Int(6)), s)
 
 
-T = TypeVar("T", bound=BaseType)
-
-
 class StringTypeSpec(DynamicArrayTypeSpec):
     def __init__(self) -> None:
         super().__init__(ByteTypeSpec())
@@ -70,12 +67,7 @@ def set(
 
         match value:
             case ComputedValue():
-                if value.produced_type_spec() == StringTypeSpec():
-                    return value.store_into(self)
-
-                raise TealInputError(
-                    f"Got ComputedValue with type spec {value.produced_type_spec()}, expected StringTypeSpec"
-                )
+                return self._set_with_computed_type(value)
             case BaseType():
                 if value.type_spec() == StringTypeSpec() or (
                     value.type_spec() == DynamicArrayTypeSpec(ByteTypeSpec())
diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py
index 5b39509df..b01675b6d 100644
--- a/pyteal/ast/abi/util.py
+++ b/pyteal/ast/abi/util.py
@@ -1,4 +1,4 @@
-from typing import TypeVar, Any, Literal, get_origin, get_args, cast
+from typing import Sequence, TypeVar, Any, Literal, get_origin, get_args, cast
 
 import algosdk.abi
 
@@ -235,6 +235,29 @@ def type_spec_from_annotation(annotation: Any) -> TypeSpec:
 T = TypeVar("T", bound=BaseType)
 
 
+def contains_type_spec(ts: TypeSpec, targets: Sequence[TypeSpec]) -> bool:
+    from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec
+    from pyteal.ast.abi.array_static import StaticArrayTypeSpec
+    from pyteal.ast.abi.tuple import TupleTypeSpec
+
+    stack: list[TypeSpec] = [ts]
+
+    while stack:
+        current = stack.pop()
+        if current in targets:
+            return True
+
+        match current:
+            case TupleTypeSpec():
+                stack.extend(current.value_type_specs())
+            case DynamicArrayTypeSpec():
+                stack.append(current.value_type_spec())
+            case StaticArrayTypeSpec():
+                stack.append(current.value_type_spec())
+
+    return False
+
+
 def size_of(t: type[T]) -> int:
     """Get the size in bytes of an ABI type. Must be a static type"""
 
diff --git a/pyteal/ast/return_.py b/pyteal/ast/return_.py
index 3d2b809d1..a19d894f0 100644
--- a/pyteal/ast/return_.py
+++ b/pyteal/ast/return_.py
@@ -73,10 +73,7 @@ def __teal__(self, options: "CompileOptions"):
                 )
             op = Op.return_
 
-        args = []
-        if self.value is not None:
-            args.append(self.value)
-
+        args = [] if self.value is None else [self.value]
         return TealBlock.FromOp(options, TealOp(self, op), *args)
 
     def __str__(self):
diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py
new file mode 100644
index 000000000..71b8f35b3
--- /dev/null
+++ b/pyteal/ast/router.py
@@ -0,0 +1,643 @@
+from dataclasses import dataclass, field, fields, astuple
+from typing import cast, Optional, Callable
+from enum import IntFlag
+
+from algosdk import abi as sdk_abi
+from algosdk import encoding
+
+from pyteal.config import METHOD_ARG_NUM_CUTOFF
+from pyteal.errors import TealInputError, TealInternalError
+from pyteal.types import TealType
+from pyteal.compiler.compiler import compileTeal, DEFAULT_TEAL_VERSION, OptimizeOptions
+from pyteal.ir.ops import Mode
+
+from pyteal.ast import abi
+from pyteal.ast.subroutine import (
+    OutputKwArgInfo,
+    SubroutineFnWrapper,
+    ABIReturnSubroutine,
+)
+from pyteal.ast.assert_ import Assert
+from pyteal.ast.cond import Cond
+from pyteal.ast.expr import Expr
+from pyteal.ast.app import OnComplete, EnumInt
+from pyteal.ast.int import Int
+from pyteal.ast.seq import Seq
+from pyteal.ast.methodsig import MethodSignature
+from pyteal.ast.naryexpr import And, Or
+from pyteal.ast.txn import Txn
+from pyteal.ast.return_ import Approve
+
+
+class CallConfig(IntFlag):
+    """
+    CallConfig: a "bitset"-like class for more fine-grained control over
+    `call or create` for a method about an OnComplete case.
+
+    This enumeration class allows for specifying one of the four following cases:
+    - CALL
+    - CREATE
+    - ALL
+    - NEVER
+    for a method call on one on_complete case.
+    """
+
+    NEVER = 0
+    CALL = 1
+    CREATE = 2
+    ALL = 3
+
+    def condition_under_config(self) -> Expr | int:
+        match self:
+            case CallConfig.NEVER:
+                return 0
+            case CallConfig.CALL:
+                return Txn.application_id() != Int(0)
+            case CallConfig.CREATE:
+                return Txn.application_id() == Int(0)
+            case CallConfig.ALL:
+                return 1
+            case _:
+                raise TealInternalError(f"unexpected CallConfig {self}")
+
+
+CallConfig.__module__ = "pyteal"
+
+
+@dataclass(frozen=True)
+class MethodConfig:
+    """
+    MethodConfig keep track of one method's CallConfigs for all OnComplete cases.
+
+    The `MethodConfig` implementation generalized contract method call such that the registered
+    method call is paired with certain OnCompletion conditions and creation conditions.
+    """
+
+    no_op: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+    opt_in: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+    close_out: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+    clear_state: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+    update_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+    delete_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+
+    def is_never(self) -> bool:
+        return all(map(lambda cc: cc == CallConfig.NEVER, astuple(self)))
+
+    def approval_cond(self) -> Expr | int:
+        config_oc_pairs: list[tuple[CallConfig, EnumInt]] = [
+            (self.no_op, OnComplete.NoOp),
+            (self.opt_in, OnComplete.OptIn),
+            (self.close_out, OnComplete.CloseOut),
+            (self.update_application, OnComplete.UpdateApplication),
+            (self.delete_application, OnComplete.DeleteApplication),
+        ]
+        if all(config == CallConfig.NEVER for config, _ in config_oc_pairs):
+            return 0
+        elif all(config == CallConfig.ALL for config, _ in config_oc_pairs):
+            return 1
+        else:
+            cond_list = []
+            for config, oc in config_oc_pairs:
+                config_cond = config.condition_under_config()
+                match config_cond:
+                    case Expr():
+                        cond_list.append(And(Txn.on_completion() == oc, config_cond))
+                    case 1:
+                        cond_list.append(Txn.on_completion() == oc)
+                    case 0:
+                        continue
+                    case _:
+                        raise TealInternalError(
+                            f"unexpected condition_under_config: {config_cond}"
+                        )
+            return Or(*cond_list)
+
+    def clear_state_cond(self) -> Expr | int:
+        return self.clear_state.condition_under_config()
+
+
+@dataclass(frozen=True)
+class OnCompleteAction:
+    """
+    OnComplete Action, registers bare calls to one single OnCompletion case.
+    """
+
+    action: Optional[Expr | SubroutineFnWrapper | ABIReturnSubroutine] = field(
+        kw_only=True, default=None
+    )
+    call_config: CallConfig = field(kw_only=True, default=CallConfig.NEVER)
+
+    def __post_init__(self):
+        if bool(self.call_config) ^ bool(self.action):
+            raise TealInputError(
+                f"action {self.action} and call_config {str(self.call_config)} contradicts"
+            )
+
+    @staticmethod
+    def never() -> "OnCompleteAction":
+        return OnCompleteAction()
+
+    @staticmethod
+    def create_only(
+        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
+    ) -> "OnCompleteAction":
+        return OnCompleteAction(action=f, call_config=CallConfig.CREATE)
+
+    @staticmethod
+    def call_only(
+        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
+    ) -> "OnCompleteAction":
+        return OnCompleteAction(action=f, call_config=CallConfig.CALL)
+
+    @staticmethod
+    def always(
+        f: Expr | SubroutineFnWrapper | ABIReturnSubroutine,
+    ) -> "OnCompleteAction":
+        return OnCompleteAction(action=f, call_config=CallConfig.ALL)
+
+    def is_empty(self) -> bool:
+        return not self.action and self.call_config == CallConfig.NEVER
+
+
+OnCompleteAction.__module__ = "pyteal"
+
+
+@dataclass(frozen=True)
+class BareCallActions:
+    """
+    BareCallActions keep track of bare-call registrations to all OnCompletion cases.
+    """
+
+    close_out: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never())
+    clear_state: OnCompleteAction = field(
+        kw_only=True, default=OnCompleteAction.never()
+    )
+    delete_application: OnCompleteAction = field(
+        kw_only=True, default=OnCompleteAction.never()
+    )
+    no_op: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never())
+    opt_in: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never())
+    update_application: OnCompleteAction = field(
+        kw_only=True, default=OnCompleteAction.never()
+    )
+
+    def is_empty(self) -> bool:
+        for action_field in fields(self):
+            action: OnCompleteAction = getattr(self, action_field.name)
+            if not action.is_empty():
+                return False
+        return True
+
+    def approval_construction(self) -> Optional[Expr]:
+        oc_action_pair: list[tuple[EnumInt, OnCompleteAction]] = [
+            (OnComplete.NoOp, self.no_op),
+            (OnComplete.OptIn, self.opt_in),
+            (OnComplete.CloseOut, self.close_out),
+            (OnComplete.UpdateApplication, self.update_application),
+            (OnComplete.DeleteApplication, self.delete_application),
+        ]
+        if all(oca.is_empty() for _, oca in oc_action_pair):
+            return None
+        conditions_n_branches: list[CondNode] = list()
+        for oc, oca in oc_action_pair:
+            if oca.is_empty():
+                continue
+            wrapped_handler = ASTBuilder.wrap_handler(
+                False,
+                cast(Expr | SubroutineFnWrapper | ABIReturnSubroutine, oca.action),
+            )
+            match oca.call_config:
+                case CallConfig.ALL:
+                    cond_body = wrapped_handler
+                case CallConfig.CALL | CallConfig.CREATE:
+                    cond_body = Seq(
+                        Assert(cast(Expr, oca.call_config.condition_under_config())),
+                        wrapped_handler,
+                    )
+                case _:
+                    raise TealInternalError(
+                        f"Unexpected CallConfig: {str(oca.call_config)}"
+                    )
+            conditions_n_branches.append(
+                CondNode(
+                    Txn.on_completion() == oc,
+                    cond_body,
+                )
+            )
+        return Cond(*[[n.condition, n.branch] for n in conditions_n_branches])
+
+    def clear_state_construction(self) -> Optional[Expr]:
+        if self.clear_state.is_empty():
+            return None
+
+        wrapped_handler = ASTBuilder.wrap_handler(
+            False,
+            cast(
+                Expr | SubroutineFnWrapper | ABIReturnSubroutine,
+                self.clear_state.action,
+            ),
+        )
+        match self.clear_state.call_config:
+            case CallConfig.ALL:
+                return wrapped_handler
+            case CallConfig.CALL | CallConfig.CREATE:
+                return Seq(
+                    Assert(
+                        cast(
+                            Expr, self.clear_state.call_config.condition_under_config()
+                        )
+                    ),
+                    wrapped_handler,
+                )
+            case _:
+                raise TealInternalError(
+                    f"Unexpected CallConfig: {str(self.clear_state.call_config)}"
+                )
+
+
+BareCallActions.__module__ = "pyteal"
+
+
+@dataclass(frozen=True)
+class CondNode:
+    condition: Expr
+    branch: Expr
+
+
+CondNode.__module__ = "pyteal"
+
+
+@dataclass
+class ASTBuilder:
+    conditions_n_branches: list[CondNode] = field(default_factory=list)
+
+    @staticmethod
+    def wrap_handler(
+        is_method_call: bool, handler: ABIReturnSubroutine | SubroutineFnWrapper | Expr
+    ) -> Expr:
+        """This is a helper function that handles transaction arguments passing in bare-app-call/abi-method handlers.
+
+        If `is_method_call` is True, then it can only be `ABIReturnSubroutine`,
+        otherwise:
+            - both `ABIReturnSubroutine` and `Subroutine` takes 0 argument on the stack.
+            - all three cases have none (or void) type.
+
+        On ABI method case, if the ABI method has more than 15 args, this function manages to de-tuple
+        the last (16-th) Txn app-arg into a list of ABI method arguments, and pass in to the ABI method.
+
+        Args:
+            is_method_call: a boolean value that specify if the handler is an ABI method.
+            handler: an `ABIReturnSubroutine`, or `SubroutineFnWrapper` (for `Subroutine` case), or an `Expr`.
+        Returns:
+            Expr:
+                - for bare-appcall it returns an expression that the handler takes no txn arg and Approve
+                - for abi-method it returns the txn args correctly decomposed into ABI variables,
+                  passed in ABIReturnSubroutine and logged, then approve.
+        """
+        if not is_method_call:
+            match handler:
+                case Expr():
+                    if handler.type_of() != TealType.none:
+                        raise TealInputError(
+                            f"bare appcall handler should be TealType.none not {handler.type_of()}."
+                        )
+                    return handler if handler.has_return() else Seq(handler, Approve())
+                case SubroutineFnWrapper():
+                    if handler.type_of() != TealType.none:
+                        raise TealInputError(
+                            f"subroutine call should be returning TealType.none not {handler.type_of()}."
+                        )
+                    if handler.subroutine.argument_count() != 0:
+                        raise TealInputError(
+                            f"subroutine call should take 0 arg for bare-app call. "
+                            f"this subroutine takes {handler.subroutine.argument_count()}."
+                        )
+                    return Seq(handler(), Approve())
+                case ABIReturnSubroutine():
+                    if handler.type_of() != "void":
+                        raise TealInputError(
+                            f"abi-returning subroutine call should be returning void not {handler.type_of()}."
+                        )
+                    if handler.subroutine.argument_count() != 0:
+                        raise TealInputError(
+                            f"abi-returning subroutine call should take 0 arg for bare-app call. "
+                            f"this abi-returning subroutine takes {handler.subroutine.argument_count()}."
+                        )
+                    return Seq(cast(Expr, handler()), Approve())
+                case _:
+                    raise TealInputError(
+                        "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg"
+                    )
+        else:
+            if not isinstance(handler, ABIReturnSubroutine):
+                raise TealInputError(
+                    f"method call should be only registering ABIReturnSubroutine, got {type(handler)}."
+                )
+            if not handler.is_abi_routable():
+                raise TealInputError(
+                    f"method call ABIReturnSubroutine is not routable "
+                    f"got {handler.subroutine.argument_count()} args with {len(handler.subroutine.abi_args)} ABI args."
+                )
+
+            arg_type_specs = cast(
+                list[abi.TypeSpec], handler.subroutine.expected_arg_types
+            )
+            if handler.subroutine.argument_count() > METHOD_ARG_NUM_CUTOFF:
+                last_arg_specs_grouped = arg_type_specs[METHOD_ARG_NUM_CUTOFF - 1 :]
+                arg_type_specs = arg_type_specs[: METHOD_ARG_NUM_CUTOFF - 1]
+                last_arg_spec = abi.TupleTypeSpec(*last_arg_specs_grouped)
+                arg_type_specs.append(last_arg_spec)
+
+            arg_abi_vars: list[abi.BaseType] = [
+                type_spec.new_instance() for type_spec in arg_type_specs
+            ]
+            decode_instructions: list[Expr] = [
+                arg_abi_vars[i].decode(Txn.application_args[i + 1])
+                for i in range(len(arg_type_specs))
+            ]
+
+            if handler.subroutine.argument_count() > METHOD_ARG_NUM_CUTOFF:
+                tuple_arg_type_specs: list[abi.TypeSpec] = cast(
+                    list[abi.TypeSpec],
+                    handler.subroutine.expected_arg_types[METHOD_ARG_NUM_CUTOFF - 1 :],
+                )
+                tuple_abi_args: list[abi.BaseType] = [
+                    t_arg_ts.new_instance() for t_arg_ts in tuple_arg_type_specs
+                ]
+                last_tuple_arg: abi.Tuple = cast(abi.Tuple, arg_abi_vars[-1])
+                de_tuple_instructions: list[Expr] = [
+                    last_tuple_arg[i].store_into(tuple_abi_args[i])
+                    for i in range(len(tuple_arg_type_specs))
+                ]
+                decode_instructions += de_tuple_instructions
+                arg_abi_vars = arg_abi_vars[:-1] + tuple_abi_args
+
+            # NOTE: does not have to have return, can be void method
+            if handler.type_of() == "void":
+                return Seq(
+                    *decode_instructions,
+                    cast(Expr, handler(*arg_abi_vars)),
+                    Approve(),
+                )
+            else:
+                output_temp: abi.BaseType = cast(
+                    OutputKwArgInfo, handler.output_kwarg_info
+                ).abi_type.new_instance()
+                subroutine_call: abi.ReturnedValue = cast(
+                    abi.ReturnedValue, handler(*arg_abi_vars)
+                )
+                return Seq(
+                    *decode_instructions,
+                    subroutine_call.store_into(output_temp),
+                    abi.MethodReturn(output_temp),
+                    Approve(),
+                )
+
+    def add_method_to_ast(
+        self, method_signature: str, cond: Expr | int, handler: ABIReturnSubroutine
+    ) -> None:
+        walk_in_cond = Txn.application_args[0] == MethodSignature(method_signature)
+        match cond:
+            case Expr():
+                self.conditions_n_branches.append(
+                    CondNode(
+                        walk_in_cond,
+                        Seq(Assert(cond), self.wrap_handler(True, handler)),
+                    )
+                )
+            case 1:
+                self.conditions_n_branches.append(
+                    CondNode(walk_in_cond, self.wrap_handler(True, handler))
+                )
+            case 0:
+                return
+            case _:
+                raise TealInputError("Invalid condition input for add_method_to_ast")
+
+    def program_construction(self) -> Expr:
+        if not self.conditions_n_branches:
+            raise TealInputError("ABIRouter: Cannot build program with an empty AST")
+        return Cond(*[[n.condition, n.branch] for n in self.conditions_n_branches])
+
+
+class Router:
+    """
+    Class that help constructs:
+    - a *Generalized* ARC-4 app's approval/clear-state programs
+    - and a contract JSON object allowing for easily read and call methods in the contract
+
+    *DISCLAIMER*: ABI-Router is still taking shape and is subject to backwards incompatible changes.
+
+    * Based on feedback, the API and usage patterns are likely to change.
+    * Expect migration issues.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        bare_calls: BareCallActions = None,
+    ) -> None:
+        """
+        Args:
+            name: the name of the smart contract, used in the JSON object.
+            bare_calls: the bare app call registered for each on_completion.
+        """
+
+        self.name: str = name
+        self.approval_ast = ASTBuilder()
+        self.clear_state_ast = ASTBuilder()
+
+        self.method_sig_to_selector: dict[str, bytes] = dict()
+        self.method_selector_to_sig: dict[bytes, str] = dict()
+
+        if bare_calls and not bare_calls.is_empty():
+            bare_call_approval = bare_calls.approval_construction()
+            if bare_call_approval:
+                self.approval_ast.conditions_n_branches.append(
+                    CondNode(
+                        Txn.application_args.length() == Int(0),
+                        cast(Expr, bare_call_approval),
+                    )
+                )
+            bare_call_clear = bare_calls.clear_state_construction()
+            if bare_call_clear:
+                self.clear_state_ast.conditions_n_branches.append(
+                    CondNode(
+                        Txn.application_args.length() == Int(0),
+                        cast(Expr, bare_call_clear),
+                    )
+                )
+
+    def add_method_handler(
+        self,
+        method_call: ABIReturnSubroutine,
+        overriding_name: str = None,
+        method_config: MethodConfig = None,
+    ) -> None:
+        if not isinstance(method_call, ABIReturnSubroutine):
+            raise TealInputError(
+                "for adding method handler, must be ABIReturnSubroutine"
+            )
+        method_signature = method_call.method_signature(overriding_name)
+        if method_config is None:
+            method_config = MethodConfig(no_op=CallConfig.CALL)
+        if method_config.is_never():
+            raise TealInputError(
+                f"registered method {method_signature} is never executed"
+            )
+        method_selector = encoding.checksum(bytes(method_signature, "utf-8"))[:4]
+
+        if method_signature in self.method_sig_to_selector:
+            raise TealInputError(f"re-registering method {method_signature} detected")
+        if method_selector in self.method_selector_to_sig:
+            raise TealInputError(
+                f"re-registering method {method_signature} has hash collision "
+                f"with {self.method_selector_to_sig[method_selector]}"
+            )
+        self.method_sig_to_selector[method_signature] = method_selector
+        self.method_selector_to_sig[method_selector] = method_signature
+
+        method_approval_cond = method_config.approval_cond()
+        method_clear_state_cond = method_config.clear_state_cond()
+        self.approval_ast.add_method_to_ast(
+            method_signature, method_approval_cond, method_call
+        )
+        self.clear_state_ast.add_method_to_ast(
+            method_signature, method_clear_state_cond, method_call
+        )
+
+    def method(
+        self,
+        func: Callable = None,
+        /,
+        *,
+        name: str = None,
+        no_op: CallConfig = None,
+        opt_in: CallConfig = None,
+        close_out: CallConfig = None,
+        clear_state: CallConfig = None,
+        update_application: CallConfig = None,
+        delete_application: CallConfig = None,
+    ):
+        """
+        A decorator style method registration by decorating over a python function,
+        which is internally converted to ABIReturnSubroutine, and taking keyword arguments
+        for each OnCompletes' `CallConfig`.
+
+        NOTE:
+            By default, all OnCompletes other than `NoOp` are set to `CallConfig.NEVER`,
+            while `no_op` field is always `CALL`.
+            If one wants to change `no_op`,  we need to change `no_op = CallConfig.ALL`,
+            for example, as a decorator argument.
+        """
+
+        # we use `is None` extensively for CallConfig to distinguish 2 following cases
+        # - None
+        # - CallConfig.Never
+        # both cases evaluate to False in if statement.
+        def wrap(_func):
+            wrapped_subroutine = ABIReturnSubroutine(_func)
+            call_configs: MethodConfig
+            if (
+                no_op is None
+                and opt_in is None
+                and close_out is None
+                and clear_state is None
+                and update_application is None
+                and delete_application is None
+            ):
+                call_configs = MethodConfig(no_op=CallConfig.CALL)
+            else:
+
+                def none_to_never(x: None | CallConfig):
+                    return CallConfig.NEVER if x is None else x
+
+                _no_op = none_to_never(no_op)
+                _opt_in = none_to_never(opt_in)
+                _close_out = none_to_never(close_out)
+                _clear_state = none_to_never(clear_state)
+                _update_app = none_to_never(update_application)
+                _delete_app = none_to_never(delete_application)
+
+                call_configs = MethodConfig(
+                    no_op=_no_op,
+                    opt_in=_opt_in,
+                    close_out=_close_out,
+                    clear_state=_clear_state,
+                    update_application=_update_app,
+                    delete_application=_delete_app,
+                )
+            self.add_method_handler(wrapped_subroutine, name, call_configs)
+
+        if not func:
+            return wrap
+        return wrap(func)
+
+    def contract_construct(self) -> sdk_abi.Contract:
+        """A helper function in constructing contract JSON object.
+
+        It takes out the method signatures from approval program `ProgramNode`'s,
+        and constructs an `Contract` object.
+
+        Returns:
+            contract: a dictified `Contract` object constructed from
+                approval program's method signatures and `self.name`.
+        """
+        method_collections = [
+            sdk_abi.Method.from_signature(sig)
+            for sig in self.method_sig_to_selector
+            if isinstance(sig, str)
+        ]
+        return sdk_abi.Contract(self.name, method_collections)
+
+    def build_program(self) -> tuple[Expr, Expr, sdk_abi.Contract]:
+        """
+        Constructs ASTs for approval and clear-state programs from the registered methods in the router,
+        also generates a JSON object of contract to allow client read and call the methods easily.
+
+        Returns:
+            approval_program: AST for approval program
+            clear_state_program: AST for clear-state program
+            contract: JSON object of contract to allow client start off-chain call
+        """
+        return (
+            self.approval_ast.program_construction(),
+            self.clear_state_ast.program_construction(),
+            self.contract_construct(),
+        )
+
+    def compile_program(
+        self,
+        *,
+        version: int = DEFAULT_TEAL_VERSION,
+        assemble_constants: bool = False,
+        optimize: OptimizeOptions = None,
+    ) -> tuple[str, str, sdk_abi.Contract]:
+        """
+        Combining `build_program` and `compileTeal`, compiles built Approval and ClearState programs
+        and returns Contract JSON object for off-chain calling.
+
+        Returns:
+            approval_program: compiled approval program
+            clear_state_program: compiled clear-state program
+            contract: JSON object of contract to allow client start off-chain call
+        """
+        ap, csp, contract = self.build_program()
+        ap_compiled = compileTeal(
+            ap,
+            Mode.Application,
+            version=version,
+            assembleConstants=assemble_constants,
+            optimize=optimize,
+        )
+        csp_compiled = compileTeal(
+            csp,
+            Mode.Application,
+            version=version,
+            assembleConstants=assemble_constants,
+            optimize=optimize,
+        )
+        return ap_compiled, csp_compiled, contract
+
+
+Router.__module__ = "pyteal"
diff --git a/pyteal/ast/subroutine.py b/pyteal/ast/subroutine.py
index c7ef8135d..4a8dec550 100644
--- a/pyteal/ast/subroutine.py
+++ b/pyteal/ast/subroutine.py
@@ -589,6 +589,28 @@ def __call__(
     def name(self) -> str:
         return self.subroutine.name()
 
+    def method_signature(self, overriding_name: str = None) -> str:
+        if not self.is_abi_routable():
+            raise TealInputError(
+                "Only registrable methods may return a method signature"
+            )
+
+        ret_type = self.type_of()
+        if isinstance(ret_type, abi.TypeSpec) and abi.contains_type_spec(
+            ret_type,
+            [
+                abi.AccountTypeSpec(),
+                abi.AssetTypeSpec(),
+                abi.ApplicationTypeSpec(),
+            ],
+        ):
+            raise TealInputError("Reference types may not be used as return values")
+
+        args = [str(v) for v in self.subroutine.abi_args.values()]
+        if overriding_name is None:
+            overriding_name = self.name()
+        return f"{overriding_name}({','.join(args)}){self.type_of()}"
+
     def type_of(self) -> str | abi.TypeSpec:
         return (
             "void"
@@ -596,7 +618,7 @@ def type_of(self) -> str | abi.TypeSpec:
             else self.output_kwarg_info.abi_type
         )
 
-    def is_registrable(self) -> bool:
+    def is_abi_routable(self) -> bool:
         return len(self.subroutine.abi_args) == self.subroutine.argument_count()
 
 
diff --git a/pyteal/compiler/constants.py b/pyteal/compiler/constants.py
index b47215588..415b7d19e 100644
--- a/pyteal/compiler/constants.py
+++ b/pyteal/compiler/constants.py
@@ -104,7 +104,7 @@ def extractMethodSigValue(op: TealOp) -> bytes:
         methodSignature = methodSignature[1:-1]
     else:
         raise TealInternalError(
-            "Method signature opcode error: signatue {} not wrapped with double-quotes".format(
+            "Method signature opcode error: signature {} not wrapped with double-quotes".format(
                 methodSignature
             )
         )
diff --git a/pyteal/config.py b/pyteal/config.py
index 7bbffd9be..e32fda458 100644
--- a/pyteal/config.py
+++ b/pyteal/config.py
@@ -1,3 +1,6 @@
+from algosdk.atomic_transaction_composer import ABI_RETURN_HASH
+
+
 # Maximum size of an atomic transaction group.
 MAX_GROUP_SIZE = 16
 
@@ -5,4 +8,7 @@
 NUM_SLOTS = 256
 
 # Method return selector in base16
-RETURN_METHOD_SELECTOR = "0x151F7C75"
+RETURN_HASH_PREFIX = ABI_RETURN_HASH
+
+# Method argument number limit
+METHOD_ARG_NUM_CUTOFF = 15
diff --git a/pyteal/ir/tealcomponent.py b/pyteal/ir/tealcomponent.py
index bb7755879..f723e20f6 100644
--- a/pyteal/ir/tealcomponent.py
+++ b/pyteal/ir/tealcomponent.py
@@ -67,6 +67,21 @@ def __exit__(self, *args):
 
         @classmethod
         def ignoreScratchSlotEquality(cls):
+            """When comparing TealOps, do not verify the equality of any ScratchSlot arguments.
+
+            This is commonly used in testing to verify the that two control flow graphs contains the
+            same operations, but may use different ScratchSlots in them. In this case, you will most
+            likely want to also use use the following code after comparing with this option enabled:
+
+                .. code-block:: python
+                    TealBlock.MatchScratchSlotReferences(
+                        TealBlock.GetReferencedScratchSlots(actual),
+                        TealBlock.GetReferencedScratchSlots(expected),
+                    )
+
+            This ensures that the ScratchSlot usages between the two control flow graphs is
+            equivalent. See :any:`TealBlock.MatchScratchSlotReferences` for more info.
+            """
             return cls.ScratchSlotEqualityContext()
 
 


Test patch:
diff --git a/pyteal/ast/router_test.py b/pyteal/ast/router_test.py
new file mode 100644
index 000000000..d9b6a967a
--- /dev/null
+++ b/pyteal/ast/router_test.py
@@ -0,0 +1,535 @@
+import pyteal as pt
+from pyteal.ast.router import ASTBuilder
+import pytest
+import typing
+import algosdk.abi as sdk_abi
+
+
+options = pt.CompileOptions(version=5)
+
+
+@pt.ABIReturnSubroutine
+def add(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+    return output.set(a.get() + b.get())
+
+
+@pt.ABIReturnSubroutine
+def sub(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+    return output.set(a.get() - b.get())
+
+
+@pt.ABIReturnSubroutine
+def mul(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+    return output.set(a.get() * b.get())
+
+
+@pt.ABIReturnSubroutine
+def div(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+    return output.set(a.get() / b.get())
+
+
+@pt.ABIReturnSubroutine
+def mod(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:
+    return output.set(a.get() % b.get())
+
+
+@pt.ABIReturnSubroutine
+def qrem(
+    a: pt.abi.Uint64,
+    b: pt.abi.Uint64,
+    *,
+    output: pt.abi.Tuple2[pt.abi.Uint64, pt.abi.Uint64],
+) -> pt.Expr:
+    return pt.Seq(
+        (q := pt.abi.Uint64()).set(a.get() / b.get()),
+        (rem := pt.abi.Uint64()).set(a.get() % b.get()),
+        output.set(q, rem),
+    )
+
+
+@pt.ABIReturnSubroutine
+def reverse(a: pt.abi.String, *, output: pt.abi.String) -> pt.Expr:
+    idx = pt.ScratchVar()
+    buff = pt.ScratchVar()
+
+    init = idx.store(pt.Int(0))
+    cond = idx.load() < a.length()
+    _iter = idx.store(idx.load() + pt.Int(1))
+    return pt.Seq(
+        buff.store(pt.Bytes("")),
+        pt.For(init, cond, _iter).Do(
+            a[idx.load()].use(lambda v: buff.store(pt.Concat(v.encode(), buff.load())))
+        ),
+        output.set(buff.load()),
+    )
+
+
+@pt.ABIReturnSubroutine
+def concat_strings(
+    b: pt.abi.DynamicArray[pt.abi.String], *, output: pt.abi.String
+) -> pt.Expr:
+    idx = pt.ScratchVar()
+    buff = pt.ScratchVar()
+
+    init = idx.store(pt.Int(0))
+    cond = idx.load() < b.length()
+    _iter = idx.store(idx.load() + pt.Int(1))
+    return pt.Seq(
+        buff.store(pt.Bytes("")),
+        pt.For(init, cond, _iter).Do(
+            b[idx.load()].use(lambda s: buff.store(pt.Concat(buff.load(), s.get())))
+        ),
+        output.set(buff.load()),
+    )
+
+
+@pt.ABIReturnSubroutine
+def many_args(
+    _a: pt.abi.Uint64,
+    _b: pt.abi.Uint64,
+    _c: pt.abi.Uint64,
+    _d: pt.abi.Uint64,
+    _e: pt.abi.Uint64,
+    _f: pt.abi.Uint64,
+    _g: pt.abi.Uint64,
+    _h: pt.abi.Uint64,
+    _i: pt.abi.Uint64,
+    _j: pt.abi.Uint64,
+    _k: pt.abi.Uint64,
+    _l: pt.abi.Uint64,
+    _m: pt.abi.Uint64,
+    _n: pt.abi.Uint64,
+    _o: pt.abi.Uint64,
+    _p: pt.abi.Uint64,
+    _q: pt.abi.Uint64,
+    _r: pt.abi.Uint64,
+    _s: pt.abi.Uint64,
+    _t: pt.abi.Uint64,
+    *,
+    output: pt.abi.Uint64,
+) -> pt.Expr:
+    return output.set(_t.get())
+
+
+@pt.Subroutine(pt.TealType.none)
+def safe_clear_state_delete():
+    return (
+        pt.If(pt.Txn.sender() == pt.Global.creator_address())
+        .Then(pt.Approve())
+        .Else(pt.Reject())
+    )
+
+
+@pt.ABIReturnSubroutine
+def dummy_doing_nothing():
+    return pt.Seq(pt.Log(pt.Bytes("a message")))
+
+
+@pt.Subroutine(pt.TealType.uint64)
+def returning_u64():
+    return pt.Int(1)
+
+
+@pt.Subroutine(pt.TealType.none)
+def mult_over_u64_and_log(a: pt.Expr, b: pt.Expr):
+    return pt.Log(pt.Itob(a * b))
+
+
+@pt.ABIReturnSubroutine
+def eine_constant(*, output: pt.abi.Uint64):
+    return output.set(1)
+
+
+@pt.ABIReturnSubroutine
+def take_abi_and_log(tb_logged: pt.abi.String):
+    return pt.Log(tb_logged.get())
+
+
+@pt.ABIReturnSubroutine
+def not_registrable(lhs: pt.abi.Uint64, rhs: pt.Expr, *, output: pt.abi.Uint64):
+    return output.set(lhs.get() * rhs)
+
+
+GOOD_SUBROUTINE_CASES: list[pt.ABIReturnSubroutine | pt.SubroutineFnWrapper] = [
+    add,
+    sub,
+    mul,
+    div,
+    mod,
+    qrem,
+    reverse,
+    concat_strings,
+    many_args,
+    safe_clear_state_delete,
+    dummy_doing_nothing,
+    eine_constant,
+    take_abi_and_log,
+]
+
+ON_COMPLETE_CASES: list[pt.EnumInt] = [
+    pt.OnComplete.NoOp,
+    pt.OnComplete.OptIn,
+    pt.OnComplete.ClearState,
+    pt.OnComplete.CloseOut,
+    pt.OnComplete.UpdateApplication,
+    pt.OnComplete.DeleteApplication,
+]
+
+
+def power_set(no_dup_list: list, length_override: int = None):
+    """
+    This function serves as a generator for all possible elements in power_set
+    over `non_dup_list`, which is a list of non-duplicated elements (matches property of a set).
+
+    The cardinality of a powerset is 2^|non_dup_list|, so we can iterate from 0 to 2^|non_dup_list| - 1
+    to index each element in such power_set.
+    By binary representation of each index, we can see it as an allowance over each element in `no_dup_list`,
+    and generate a unique subset of `non_dup_list`, which yields as an element of power_set of `no_dup_list`.
+
+    Args:
+        no_dup_list: a list of elements with no duplication
+        length_override: a number indicating the largest size of super_set element,
+            must be in range [1, len(no_dup_list)].
+    """
+    if length_override is None:
+        length_override = len(no_dup_list)
+    assert 1 <= length_override <= len(no_dup_list)
+    masks = [1 << i for i in range(length_override)]
+    for i in range(1 << len(no_dup_list)):
+        yield [elem for mask, elem in zip(masks, no_dup_list) if i & mask]
+
+
+def full_ordered_combination_gen(non_dup_list: list, perm_length: int):
+    """
+    This function serves as a generator for all possible vectors of length `perm_length`,
+    each of whose entries are one of the elements in `non_dup_list`,
+    which is a list of non-duplicated elements.
+
+    Args:
+        non_dup_list: must be a list of elements with no duplication
+        perm_length: must be a non-negative number indicating resulting length of the vector
+    """
+    if perm_length < 0:
+        raise pt.TealInputError("input permutation length must be non-negative")
+    elif len(set(non_dup_list)) != len(non_dup_list):
+        raise pt.TealInputError(f"input non_dup_list {non_dup_list} has duplications")
+    elif perm_length == 0:
+        yield []
+        return
+    # we can index all possible cases of vectors with an index in range
+    # [0, |non_dup_list| ^ perm_length - 1]
+    # by converting an index into |non_dup_list|-based number,
+    # we can get the vector mapped by the index.
+    for index in range(len(non_dup_list) ** perm_length):
+        index_list_basis = []
+        temp = index
+        for _ in range(perm_length):
+            index_list_basis.append(non_dup_list[temp % len(non_dup_list)])
+            temp //= len(non_dup_list)
+        yield index_list_basis
+
+
+def oncomplete_is_in_oc_list(sth: pt.EnumInt, oc_list: list[pt.EnumInt]):
+    return any(map(lambda x: str(x) == str(sth), oc_list))
+
+
+def assemble_helper(what: pt.Expr) -> pt.TealBlock:
+    assembled, _ = what.__teal__(options)
+    assembled.addIncoming()
+    assembled = pt.TealBlock.NormalizeBlocks(assembled)
+    return assembled
+
+
+def camel_to_snake(name: str) -> str:
+    return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
+
+
+def test_call_config():
+    for cc in pt.CallConfig:
+        cond_on_cc: pt.Expr | int = cc.condition_under_config()
+        match cond_on_cc:
+            case pt.Expr():
+                expected_cc = (
+                    (pt.Txn.application_id() == pt.Int(0))
+                    if cc == pt.CallConfig.CREATE
+                    else (pt.Txn.application_id() != pt.Int(0))
+                )
+                with pt.TealComponent.Context.ignoreExprEquality():
+                    assert assemble_helper(cond_on_cc) == assemble_helper(expected_cc)
+            case int():
+                assert cond_on_cc == int(cc) & 1
+            case _:
+                raise pt.TealInternalError(f"unexpected cond_on_cc {cond_on_cc}")
+
+
+def test_method_config():
+    never_mc = pt.MethodConfig(no_op=pt.CallConfig.NEVER)
+    assert never_mc.is_never()
+    assert never_mc.approval_cond() == 0
+    assert never_mc.clear_state_cond() == 0
+
+    on_complete_pow_set = power_set(ON_COMPLETE_CASES)
+    approval_check_names_n_ocs = [
+        (camel_to_snake(oc.name), oc)
+        for oc in ON_COMPLETE_CASES
+        if str(oc) != str(pt.OnComplete.ClearState)
+    ]
+    for on_complete_set in on_complete_pow_set:
+        oc_names = [camel_to_snake(oc.name) for oc in on_complete_set]
+        ordered_call_configs = full_ordered_combination_gen(
+            list(pt.CallConfig), len(on_complete_set)
+        )
+        for call_configs in ordered_call_configs:
+            mc = pt.MethodConfig(**dict(zip(oc_names, call_configs)))
+            match mc.clear_state:
+                case pt.CallConfig.NEVER:
+                    assert mc.clear_state_cond() == 0
+                case pt.CallConfig.ALL:
+                    assert mc.clear_state_cond() == 1
+                case pt.CallConfig.CALL:
+                    with pt.TealComponent.Context.ignoreExprEquality():
+                        assert assemble_helper(
+                            mc.clear_state_cond()
+                        ) == assemble_helper(pt.Txn.application_id() != pt.Int(0))
+                case pt.CallConfig.CREATE:
+                    with pt.TealComponent.Context.ignoreExprEquality():
+                        assert assemble_helper(
+                            mc.clear_state_cond()
+                        ) == assemble_helper(pt.Txn.application_id() == pt.Int(0))
+            if mc.is_never() or all(
+                getattr(mc, i) == pt.CallConfig.NEVER
+                for i, _ in approval_check_names_n_ocs
+            ):
+                assert mc.approval_cond() == 0
+                continue
+            elif all(
+                getattr(mc, i) == pt.CallConfig.ALL
+                for i, _ in approval_check_names_n_ocs
+            ):
+                assert mc.approval_cond() == 1
+                continue
+            list_of_cc = [
+                (
+                    typing.cast(pt.CallConfig, getattr(mc, i)).condition_under_config(),
+                    oc,
+                )
+                for i, oc in approval_check_names_n_ocs
+            ]
+            list_of_expressions = []
+            for expr_or_int, oc in list_of_cc:
+                match expr_or_int:
+                    case pt.Expr():
+                        list_of_expressions.append(
+                            pt.And(pt.Txn.on_completion() == oc, expr_or_int)
+                        )
+                    case 0:
+                        continue
+                    case 1:
+                        list_of_expressions.append(pt.Txn.on_completion() == oc)
+            with pt.TealComponent.Context.ignoreExprEquality():
+                assert assemble_helper(mc.approval_cond()) == assemble_helper(
+                    pt.Or(*list_of_expressions)
+                )
+
+
+def test_on_complete_action():
+    with pytest.raises(pt.TealInputError) as contradict_err:
+        pt.OnCompleteAction(action=pt.Seq(), call_config=pt.CallConfig.NEVER)
+    assert "contradicts" in str(contradict_err)
+    assert pt.OnCompleteAction.never().is_empty()
+    assert pt.OnCompleteAction.call_only(pt.Seq()).call_config == pt.CallConfig.CALL
+    assert pt.OnCompleteAction.create_only(pt.Seq()).call_config == pt.CallConfig.CREATE
+    assert pt.OnCompleteAction.always(pt.Seq()).call_config == pt.CallConfig.ALL
+
+
+def test_wrap_handler_bare_call():
+    BARE_CALL_CASES = [
+        dummy_doing_nothing,
+        safe_clear_state_delete,
+        pt.Approve(),
+        pt.Log(pt.Bytes("message")),
+    ]
+    for bare_call in BARE_CALL_CASES:
+        wrapped: pt.Expr = ASTBuilder.wrap_handler(False, bare_call)
+        expected: pt.Expr
+        match bare_call:
+            case pt.Expr():
+                if bare_call.has_return():
+                    expected = bare_call
+                else:
+                    expected = pt.Seq(bare_call, pt.Approve())
+            case pt.SubroutineFnWrapper() | pt.ABIReturnSubroutine():
+                expected = pt.Seq(bare_call(), pt.Approve())
+            case _:
+                raise pt.TealInputError("how you got here?")
+        wrapped_assemble = assemble_helper(wrapped)
+        wrapped_helper = assemble_helper(expected)
+        with pt.TealComponent.Context.ignoreExprEquality():
+            assert wrapped_assemble == wrapped_helper
+
+    ERROR_CASES = [
+        (
+            pt.Int(1),
+            f"bare appcall handler should be TealType.none not {pt.TealType.uint64}.",
+        ),
+        (
+            returning_u64,
+            f"subroutine call should be returning TealType.none not {pt.TealType.uint64}.",
+        ),
+        (
+            mult_over_u64_and_log,
+            "subroutine call should take 0 arg for bare-app call. this subroutine takes 2.",
+        ),
+        (
+            eine_constant,
+            f"abi-returning subroutine call should be returning void not {pt.abi.Uint64TypeSpec()}.",
+        ),
+        (
+            take_abi_and_log,
+            "abi-returning subroutine call should take 0 arg for bare-app call. this abi-returning subroutine takes 1.",
+        ),
+        (
+            1,
+            "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg",
+        ),
+    ]
+    for error_case, error_msg in ERROR_CASES:
+        with pytest.raises(pt.TealInputError) as bug:
+            ASTBuilder.wrap_handler(False, error_case)
+        assert error_msg in str(bug)
+
+
+def test_wrap_handler_method_call():
+    with pytest.raises(pt.TealInputError) as bug:
+        ASTBuilder.wrap_handler(True, not_registrable)
+    assert "method call ABIReturnSubroutine is not routable" in str(bug)
+
+    with pytest.raises(pt.TealInputError) as bug:
+        ASTBuilder.wrap_handler(True, safe_clear_state_delete)
+    assert "method call should be only registering ABIReturnSubroutine" in str(bug)
+
+    ONLY_ABI_SUBROUTINE_CASES = list(
+        filter(lambda x: isinstance(x, pt.ABIReturnSubroutine), GOOD_SUBROUTINE_CASES)
+    )
+    for abi_subroutine in ONLY_ABI_SUBROUTINE_CASES:
+        wrapped: pt.Expr = ASTBuilder.wrap_handler(True, abi_subroutine)
+        actual: pt.TealBlock = assemble_helper(wrapped)
+
+        args: list[pt.abi.BaseType] = [
+            spec.new_instance()
+            for spec in typing.cast(
+                list[pt.abi.TypeSpec], abi_subroutine.subroutine.expected_arg_types
+            )
+        ]
+
+        loading: list[pt.Expr]
+
+        if abi_subroutine.subroutine.argument_count() > pt.METHOD_ARG_NUM_CUTOFF:
+            sdk_last_arg = pt.abi.TupleTypeSpec(
+                *[
+                    spec
+                    for spec in typing.cast(
+                        list[pt.abi.TypeSpec],
+                        abi_subroutine.subroutine.expected_arg_types,
+                    )[pt.METHOD_ARG_NUM_CUTOFF - 1 :]
+                ]
+            ).new_instance()
+            loading = [
+                arg.decode(pt.Txn.application_args[index + 1])
+                for index, arg in enumerate(args[: pt.METHOD_ARG_NUM_CUTOFF - 1])
+            ]
+            loading.append(
+                sdk_last_arg.decode(pt.Txn.application_args[pt.METHOD_ARG_NUM_CUTOFF])
+            )
+            for i in range(pt.METHOD_ARG_NUM_CUTOFF - 1, len(args)):
+                loading.append(
+                    sdk_last_arg[i - pt.METHOD_ARG_NUM_CUTOFF + 1].store_into(args[i])
+                )
+        else:
+            loading = [
+                arg.decode(pt.Txn.application_args[index + 1])
+                for index, arg in enumerate(args)
+            ]
+
+        evaluate: pt.Expr
+        if abi_subroutine.type_of() != "void":
+            output_temp = abi_subroutine.output_kwarg_info.abi_type.new_instance()
+            evaluate = pt.Seq(
+                abi_subroutine(*args).store_into(output_temp),
+                pt.abi.MethodReturn(output_temp),
+            )
+        else:
+            evaluate = abi_subroutine(*args)
+
+        expected = assemble_helper(pt.Seq(*loading, evaluate, pt.Approve()))
+        with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality():
+            assert actual == expected
+
+        assert pt.TealBlock.MatchScratchSlotReferences(
+            pt.TealBlock.GetReferencedScratchSlots(actual),
+            pt.TealBlock.GetReferencedScratchSlots(expected),
+        )
+
+
+def test_wrap_handler_method_call_many_args():
+    wrapped: pt.Expr = ASTBuilder.wrap_handler(True, many_args)
+    actual: pt.TealBlock = assemble_helper(wrapped)
+
+    args = [pt.abi.Uint64() for _ in range(20)]
+    last_arg = pt.abi.TupleTypeSpec(
+        *[pt.abi.Uint64TypeSpec() for _ in range(6)]
+    ).new_instance()
+
+    output_temp = pt.abi.Uint64()
+    expected_ast = pt.Seq(
+        args[0].decode(pt.Txn.application_args[1]),
+        args[1].decode(pt.Txn.application_args[2]),
+        args[2].decode(pt.Txn.application_args[3]),
+        args[3].decode(pt.Txn.application_args[4]),
+        args[4].decode(pt.Txn.application_args[5]),
+        args[5].decode(pt.Txn.application_args[6]),
+        args[6].decode(pt.Txn.application_args[7]),
+        args[7].decode(pt.Txn.application_args[8]),
+        args[8].decode(pt.Txn.application_args[9]),
+        args[9].decode(pt.Txn.application_args[10]),
+        args[10].decode(pt.Txn.application_args[11]),
+        args[11].decode(pt.Txn.application_args[12]),
+        args[12].decode(pt.Txn.application_args[13]),
+        args[13].decode(pt.Txn.application_args[14]),
+        last_arg.decode(pt.Txn.application_args[15]),
+        last_arg[0].store_into(args[14]),
+        last_arg[1].store_into(args[15]),
+        last_arg[2].store_into(args[16]),
+        last_arg[3].store_into(args[17]),
+        last_arg[4].store_into(args[18]),
+        last_arg[5].store_into(args[19]),
+        many_args(*args).store_into(output_temp),
+        pt.abi.MethodReturn(output_temp),
+        pt.Approve(),
+    )
+    expected = assemble_helper(expected_ast)
+    with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality():
+        assert actual == expected
+
+    assert pt.TealBlock.MatchScratchSlotReferences(
+        pt.TealBlock.GetReferencedScratchSlots(actual),
+        pt.TealBlock.GetReferencedScratchSlots(expected),
+    )
+
+
+def test_contract_json_obj():
+    abi_subroutines = list(
+        filter(lambda x: isinstance(x, pt.ABIReturnSubroutine), GOOD_SUBROUTINE_CASES)
+    )
+    contract_name = "contract_name"
+    on_complete_actions = pt.BareCallActions(
+        clear_state=pt.OnCompleteAction.call_only(safe_clear_state_delete)
+    )
+    router = pt.Router(contract_name, on_complete_actions)
+    method_list: list[sdk_abi.Method] = []
+    for subroutine in abi_subroutines:
+        router.add_method_handler(subroutine)
+        method_list.append(sdk_abi.Method.from_signature(subroutine.method_signature()))
+    sdk_contract = sdk_abi.Contract(contract_name, method_list)
+    contract = router.contract_construct()
+    assert contract == sdk_contract
diff --git a/pyteal/ast/subroutine_test.py b/pyteal/ast/subroutine_test.py
index e01f2910a..a58df675f 100644
--- a/pyteal/ast/subroutine_test.py
+++ b/pyteal/ast/subroutine_test.py
@@ -1,11 +1,11 @@
 from itertools import product
-from typing import List, Literal
+from typing import List, Literal, Optional, cast
 
 import pytest
 from dataclasses import dataclass
 
 import pyteal as pt
-from pyteal.ast.subroutine import evaluate_subroutine
+from pyteal.ast.subroutine import ABIReturnSubroutine, evaluate_subroutine
 
 options = pt.CompileOptions(version=5)
 
@@ -86,6 +86,7 @@ class ABISubroutineTC:
     arg_instances: list[pt.Expr | pt.abi.BaseType]
     name: str
     ret_type: str | pt.abi.TypeSpec
+    signature: Optional[str]
 
 
 def test_abi_subroutine_definition():
@@ -139,13 +140,27 @@ def fn_w_tuple1arg(
         return output.set(pt.Int(1))
 
     cases = (
-        ABISubroutineTC(fn_0arg_0ret, [], "fn_0arg_0ret", "void"),
+        ABISubroutineTC(fn_0arg_0ret, [], "fn_0arg_0ret", "void", "fn_0arg_0ret()void"),
         ABISubroutineTC(
-            fn_0arg_uint64_ret, [], "fn_0arg_uint64_ret", pt.abi.Uint64TypeSpec()
+            fn_0arg_uint64_ret,
+            [],
+            "fn_0arg_uint64_ret",
+            pt.abi.Uint64TypeSpec(),
+            "fn_0arg_uint64_ret()uint64",
+        ),
+        ABISubroutineTC(
+            fn_1arg_0ret,
+            [pt.abi.Uint64()],
+            "fn_1arg_0ret",
+            "void",
+            "fn_1arg_0ret(uint64)void",
         ),
-        ABISubroutineTC(fn_1arg_0ret, [pt.abi.Uint64()], "fn_1arg_0ret", "void"),
         ABISubroutineTC(
-            fn_1arg_1ret, [pt.abi.Uint64()], "fn_1arg_1ret", pt.abi.Uint64TypeSpec()
+            fn_1arg_1ret,
+            [pt.abi.Uint64()],
+            "fn_1arg_1ret",
+            pt.abi.Uint64TypeSpec(),
+            "fn_1arg_1ret(uint64)uint64",
         ),
         ABISubroutineTC(
             fn_2arg_0ret,
@@ -157,6 +172,7 @@ def fn_w_tuple1arg(
             ],
             "fn_2arg_0ret",
             "void",
+            "fn_2arg_0ret(uint64,byte[10])void",
         ),
         ABISubroutineTC(
             fn_2arg_1ret,
@@ -168,6 +184,7 @@ def fn_w_tuple1arg(
             ],
             "fn_2arg_1ret",
             pt.abi.ByteTypeSpec(),
+            "fn_2arg_1ret(uint64,byte[10])byte",
         ),
         ABISubroutineTC(
             fn_2arg_1ret_with_expr,
@@ -179,6 +196,7 @@ def fn_w_tuple1arg(
             ],
             "fn_2arg_1ret_with_expr",
             pt.abi.ByteTypeSpec(),
+            None,
         ),
         ABISubroutineTC(
             fn_w_tuple1arg,
@@ -188,6 +206,7 @@ def fn_w_tuple1arg(
             ],
             "fn_w_tuple1arg",
             pt.abi.ByteTypeSpec(),
+            None,
         ),
     )
 
@@ -207,10 +226,47 @@ def fn_w_tuple1arg(
         assert isinstance(
             invoked, (pt.Expr if case.ret_type == "void" else pt.abi.ReturnedValue)
         )
-        assert case.definition.is_registrable() == all(
+        assert case.definition.is_abi_routable() == all(
             map(lambda x: isinstance(x, pt.abi.BaseType), case.arg_instances)
         )
 
+        if case.definition.is_abi_routable():
+            assert case.definition.method_signature() == cast(str, case.signature)
+        else:
+            with pytest.raises(pt.TealInputError):
+                case.definition.method_signature()
+
+
+def test_subroutine_return_reference():
+    @ABIReturnSubroutine
+    def invalid_ret_type(*, output: pt.abi.Account):
+        return output.set(0)
+
+    with pytest.raises(pt.TealInputError):
+        invalid_ret_type.method_signature()
+
+    @ABIReturnSubroutine
+    def invalid_ret_type_collection(
+        *, output: pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64]
+    ):
+        return output.set(pt.abi.Account(), pt.abi.Uint64())
+
+    with pytest.raises(pt.TealInputError):
+        invalid_ret_type_collection.method_signature()
+
+    @ABIReturnSubroutine
+    def invalid_ret_type_collection_nested(
+        *, output: pt.abi.DynamicArray[pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64]]
+    ):
+        return output.set(
+            pt.abi.make(
+                pt.abi.DynamicArray[pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64]]
+            )
+        )
+
+    with pytest.raises(pt.TealInputError):
+        invalid_ret_type_collection_nested.method_signature()
+
 
 def test_subroutine_definition_validate():
     """
diff --git a/pyteal/compiler/compiler_test.py b/pyteal/compiler/compiler_test.py
index a89f075fc..e20b341a2 100644
--- a/pyteal/compiler/compiler_test.py
+++ b/pyteal/compiler/compiler_test.py
@@ -2042,7 +2042,7 @@ def abi_sum(
 load 0
 callsub abisum_0
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 itob
 concat
@@ -2124,7 +2124,7 @@ def conditional_factorial(
 load 0
 callsub conditionalfactorial_0
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 itob
 concat
@@ -2193,3 +2193,1240 @@ def access_b4_store(magic_num: pt.abi.Uint64, *, output: pt.abi.Uint64):
 
     with pytest.raises(pt.TealInternalError):
         pt.compileTeal(program_access_b4_store_broken, pt.Mode.Application, version=6)
+
+
+def test_router_app():
+    def add_methods_to_router(router: pt.Router):
+        @pt.ABIReturnSubroutine
+        def add(
+            a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64
+        ) -> pt.Expr:
+            return output.set(a.get() + b.get())
+
+        router.add_method_handler(add)
+
+        @pt.ABIReturnSubroutine
+        def sub(
+            a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64
+        ) -> pt.Expr:
+            return output.set(a.get() - b.get())
+
+        router.add_method_handler(sub)
+
+        @pt.ABIReturnSubroutine
+        def mul(
+            a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64
+        ) -> pt.Expr:
+            return output.set(a.get() * b.get())
+
+        router.add_method_handler(mul)
+
+        @pt.ABIReturnSubroutine
+        def div(
+            a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64
+        ) -> pt.Expr:
+            return output.set(a.get() / b.get())
+
+        router.add_method_handler(div)
+
+        @pt.ABIReturnSubroutine
+        def mod(
+            a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64
+        ) -> pt.Expr:
+            return output.set(a.get() % b.get())
+
+        router.add_method_handler(mod)
+
+        @pt.ABIReturnSubroutine
+        def all_laid_to_args(
+            _a: pt.abi.Uint64,
+            _b: pt.abi.Uint64,
+            _c: pt.abi.Uint64,
+            _d: pt.abi.Uint64,
+            _e: pt.abi.Uint64,
+            _f: pt.abi.Uint64,
+            _g: pt.abi.Uint64,
+            _h: pt.abi.Uint64,
+            _i: pt.abi.Uint64,
+            _j: pt.abi.Uint64,
+            _k: pt.abi.Uint64,
+            _l: pt.abi.Uint64,
+            _m: pt.abi.Uint64,
+            _n: pt.abi.Uint64,
+            _o: pt.abi.Uint64,
+            _p: pt.abi.Uint64,
+            *,
+            output: pt.abi.Uint64,
+        ):
+            return output.set(
+                _a.get()
+                + _b.get()
+                + _c.get()
+                + _d.get()
+                + _e.get()
+                + _f.get()
+                + _g.get()
+                + _h.get()
+                + _i.get()
+                + _j.get()
+                + _k.get()
+                + _l.get()
+                + _m.get()
+                + _n.get()
+                + _o.get()
+                + _p.get()
+            )
+
+        router.add_method_handler(all_laid_to_args)
+
+        @pt.ABIReturnSubroutine
+        def empty_return_subroutine() -> pt.Expr:
+            return pt.Log(pt.Bytes("appear in both approval and clear state"))
+
+        router.add_method_handler(
+            empty_return_subroutine,
+            method_config=pt.MethodConfig(
+                no_op=pt.CallConfig.CALL,
+                opt_in=pt.CallConfig.ALL,
+                clear_state=pt.CallConfig.CALL,
+            ),
+        )
+
+        @pt.ABIReturnSubroutine
+        def log_1(*, output: pt.abi.Uint64) -> pt.Expr:
+            return output.set(1)
+
+        router.add_method_handler(
+            log_1,
+            method_config=pt.MethodConfig(
+                no_op=pt.CallConfig.CALL,
+                opt_in=pt.CallConfig.CALL,
+                clear_state=pt.CallConfig.CALL,
+            ),
+        )
+
+        @pt.ABIReturnSubroutine
+        def log_creation(*, output: pt.abi.String) -> pt.Expr:
+            return output.set("logging creation")
+
+        router.add_method_handler(
+            log_creation, method_config=pt.MethodConfig(no_op=pt.CallConfig.CREATE)
+        )
+
+        @pt.ABIReturnSubroutine
+        def approve_if_odd(condition_encoding: pt.abi.Uint32) -> pt.Expr:
+            return (
+                pt.If(condition_encoding.get() % pt.Int(2))
+                .Then(pt.Approve())
+                .Else(pt.Reject())
+            )
+
+        router.add_method_handler(
+            approve_if_odd,
+            method_config=pt.MethodConfig(
+                no_op=pt.CallConfig.NEVER, clear_state=pt.CallConfig.CALL
+            ),
+        )
+
+    on_completion_actions = pt.BareCallActions(
+        opt_in=pt.OnCompleteAction.call_only(pt.Log(pt.Bytes("optin call"))),
+        clear_state=pt.OnCompleteAction.call_only(pt.Approve()),
+    )
+
+    _router_with_oc = pt.Router(
+        "ASimpleQuestionablyRobustContract", on_completion_actions
+    )
+    add_methods_to_router(_router_with_oc)
+    (
+        actual_ap_with_oc_compiled,
+        actual_csp_with_oc_compiled,
+        _,
+    ) = _router_with_oc.compile_program(version=6)
+
+    expected_ap_with_oc = """#pragma version 6
+txn NumAppArgs
+int 0
+==
+bnz main_l20
+txna ApplicationArgs 0
+method "add(uint64,uint64)uint64"
+==
+bnz main_l19
+txna ApplicationArgs 0
+method "sub(uint64,uint64)uint64"
+==
+bnz main_l18
+txna ApplicationArgs 0
+method "mul(uint64,uint64)uint64"
+==
+bnz main_l17
+txna ApplicationArgs 0
+method "div(uint64,uint64)uint64"
+==
+bnz main_l16
+txna ApplicationArgs 0
+method "mod(uint64,uint64)uint64"
+==
+bnz main_l15
+txna ApplicationArgs 0
+method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64"
+==
+bnz main_l14
+txna ApplicationArgs 0
+method "empty_return_subroutine()void"
+==
+bnz main_l13
+txna ApplicationArgs 0
+method "log_1()uint64"
+==
+bnz main_l12
+txna ApplicationArgs 0
+method "log_creation()string"
+==
+bnz main_l11
+err
+main_l11:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+==
+&&
+assert
+callsub logcreation_8
+store 67
+byte 0x151f7c75
+load 67
+concat
+log
+int 1
+return
+main_l12:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+txn OnCompletion
+int OptIn
+==
+txn ApplicationID
+int 0
+!=
+&&
+||
+assert
+callsub log1_7
+store 65
+byte 0x151f7c75
+load 65
+itob
+concat
+log
+int 1
+return
+main_l13:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+txn OnCompletion
+int OptIn
+==
+||
+assert
+callsub emptyreturnsubroutine_6
+int 1
+return
+main_l14:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 30
+txna ApplicationArgs 2
+btoi
+store 31
+txna ApplicationArgs 3
+btoi
+store 32
+txna ApplicationArgs 4
+btoi
+store 33
+txna ApplicationArgs 5
+btoi
+store 34
+txna ApplicationArgs 6
+btoi
+store 35
+txna ApplicationArgs 7
+btoi
+store 36
+txna ApplicationArgs 8
+btoi
+store 37
+txna ApplicationArgs 9
+btoi
+store 38
+txna ApplicationArgs 10
+btoi
+store 39
+txna ApplicationArgs 11
+btoi
+store 40
+txna ApplicationArgs 12
+btoi
+store 41
+txna ApplicationArgs 13
+btoi
+store 42
+txna ApplicationArgs 14
+btoi
+store 43
+txna ApplicationArgs 15
+store 44
+load 44
+int 0
+extract_uint64
+store 45
+load 44
+int 8
+extract_uint64
+store 46
+load 30
+load 31
+load 32
+load 33
+load 34
+load 35
+load 36
+load 37
+load 38
+load 39
+load 40
+load 41
+load 42
+load 43
+load 45
+load 46
+callsub alllaidtoargs_5
+store 47
+byte 0x151f7c75
+load 47
+itob
+concat
+log
+int 1
+return
+main_l15:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 24
+txna ApplicationArgs 2
+btoi
+store 25
+load 24
+load 25
+callsub mod_4
+store 26
+byte 0x151f7c75
+load 26
+itob
+concat
+log
+int 1
+return
+main_l16:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 18
+txna ApplicationArgs 2
+btoi
+store 19
+load 18
+load 19
+callsub div_3
+store 20
+byte 0x151f7c75
+load 20
+itob
+concat
+log
+int 1
+return
+main_l17:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 12
+txna ApplicationArgs 2
+btoi
+store 13
+load 12
+load 13
+callsub mul_2
+store 14
+byte 0x151f7c75
+load 14
+itob
+concat
+log
+int 1
+return
+main_l18:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 6
+txna ApplicationArgs 2
+btoi
+store 7
+load 6
+load 7
+callsub sub_1
+store 8
+byte 0x151f7c75
+load 8
+itob
+concat
+log
+int 1
+return
+main_l19:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 0
+txna ApplicationArgs 2
+btoi
+store 1
+load 0
+load 1
+callsub add_0
+store 2
+byte 0x151f7c75
+load 2
+itob
+concat
+log
+int 1
+return
+main_l20:
+txn OnCompletion
+int OptIn
+==
+bnz main_l22
+err
+main_l22:
+txn ApplicationID
+int 0
+!=
+assert
+byte "optin call"
+log
+int 1
+return
+
+// add
+add_0:
+store 4
+store 3
+load 3
+load 4
++
+store 5
+load 5
+retsub
+
+// sub
+sub_1:
+store 10
+store 9
+load 9
+load 10
+-
+store 11
+load 11
+retsub
+
+// mul
+mul_2:
+store 16
+store 15
+load 15
+load 16
+*
+store 17
+load 17
+retsub
+
+// div
+div_3:
+store 22
+store 21
+load 21
+load 22
+/
+store 23
+load 23
+retsub
+
+// mod
+mod_4:
+store 28
+store 27
+load 27
+load 28
+%
+store 29
+load 29
+retsub
+
+// all_laid_to_args
+alllaidtoargs_5:
+store 63
+store 62
+store 61
+store 60
+store 59
+store 58
+store 57
+store 56
+store 55
+store 54
+store 53
+store 52
+store 51
+store 50
+store 49
+store 48
+load 48
+load 49
++
+load 50
++
+load 51
++
+load 52
++
+load 53
++
+load 54
++
+load 55
++
+load 56
++
+load 57
++
+load 58
++
+load 59
++
+load 60
++
+load 61
++
+load 62
++
+load 63
++
+store 64
+load 64
+retsub
+
+// empty_return_subroutine
+emptyreturnsubroutine_6:
+byte "appear in both approval and clear state"
+log
+retsub
+
+// log_1
+log1_7:
+int 1
+store 66
+load 66
+retsub
+
+// log_creation
+logcreation_8:
+byte "logging creation"
+len
+itob
+extract 6 0
+byte "logging creation"
+concat
+store 68
+load 68
+retsub""".strip()
+    assert expected_ap_with_oc == actual_ap_with_oc_compiled
+
+    expected_csp_with_oc = """#pragma version 6
+txn NumAppArgs
+int 0
+==
+bnz main_l8
+txna ApplicationArgs 0
+method "empty_return_subroutine()void"
+==
+bnz main_l7
+txna ApplicationArgs 0
+method "log_1()uint64"
+==
+bnz main_l6
+txna ApplicationArgs 0
+method "approve_if_odd(uint32)void"
+==
+bnz main_l5
+err
+main_l5:
+txn ApplicationID
+int 0
+!=
+assert
+txna ApplicationArgs 1
+int 0
+extract_uint32
+store 2
+load 2
+callsub approveifodd_2
+int 1
+return
+main_l6:
+txn ApplicationID
+int 0
+!=
+assert
+callsub log1_1
+store 1
+byte 0x151f7c75
+load 1
+itob
+concat
+log
+int 1
+return
+main_l7:
+txn ApplicationID
+int 0
+!=
+assert
+callsub emptyreturnsubroutine_0
+int 1
+return
+main_l8:
+txn ApplicationID
+int 0
+!=
+assert
+int 1
+return
+
+// empty_return_subroutine
+emptyreturnsubroutine_0:
+byte "appear in both approval and clear state"
+log
+retsub
+
+// log_1
+log1_1:
+int 1
+store 0
+load 0
+retsub
+
+// approve_if_odd
+approveifodd_2:
+store 3
+load 3
+int 2
+%
+bnz approveifodd_2_l2
+int 0
+return
+approveifodd_2_l2:
+int 1
+return""".strip()
+    assert expected_csp_with_oc == actual_csp_with_oc_compiled
+
+    _router_without_oc = pt.Router("yetAnotherContractConstructedFromRouter")
+    add_methods_to_router(_router_without_oc)
+    (
+        actual_ap_without_oc_compiled,
+        actual_csp_without_oc_compiled,
+        _,
+    ) = _router_without_oc.compile_program(version=6)
+
+    expected_ap_without_oc = """#pragma version 6
+txna ApplicationArgs 0
+method "add(uint64,uint64)uint64"
+==
+bnz main_l18
+txna ApplicationArgs 0
+method "sub(uint64,uint64)uint64"
+==
+bnz main_l17
+txna ApplicationArgs 0
+method "mul(uint64,uint64)uint64"
+==
+bnz main_l16
+txna ApplicationArgs 0
+method "div(uint64,uint64)uint64"
+==
+bnz main_l15
+txna ApplicationArgs 0
+method "mod(uint64,uint64)uint64"
+==
+bnz main_l14
+txna ApplicationArgs 0
+method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64"
+==
+bnz main_l13
+txna ApplicationArgs 0
+method "empty_return_subroutine()void"
+==
+bnz main_l12
+txna ApplicationArgs 0
+method "log_1()uint64"
+==
+bnz main_l11
+txna ApplicationArgs 0
+method "log_creation()string"
+==
+bnz main_l10
+err
+main_l10:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+==
+&&
+assert
+callsub logcreation_8
+store 67
+byte 0x151f7c75
+load 67
+concat
+log
+int 1
+return
+main_l11:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+txn OnCompletion
+int OptIn
+==
+txn ApplicationID
+int 0
+!=
+&&
+||
+assert
+callsub log1_7
+store 65
+byte 0x151f7c75
+load 65
+itob
+concat
+log
+int 1
+return
+main_l12:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+txn OnCompletion
+int OptIn
+==
+||
+assert
+callsub emptyreturnsubroutine_6
+int 1
+return
+main_l13:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 30
+txna ApplicationArgs 2
+btoi
+store 31
+txna ApplicationArgs 3
+btoi
+store 32
+txna ApplicationArgs 4
+btoi
+store 33
+txna ApplicationArgs 5
+btoi
+store 34
+txna ApplicationArgs 6
+btoi
+store 35
+txna ApplicationArgs 7
+btoi
+store 36
+txna ApplicationArgs 8
+btoi
+store 37
+txna ApplicationArgs 9
+btoi
+store 38
+txna ApplicationArgs 10
+btoi
+store 39
+txna ApplicationArgs 11
+btoi
+store 40
+txna ApplicationArgs 12
+btoi
+store 41
+txna ApplicationArgs 13
+btoi
+store 42
+txna ApplicationArgs 14
+btoi
+store 43
+txna ApplicationArgs 15
+store 44
+load 44
+int 0
+extract_uint64
+store 45
+load 44
+int 8
+extract_uint64
+store 46
+load 30
+load 31
+load 32
+load 33
+load 34
+load 35
+load 36
+load 37
+load 38
+load 39
+load 40
+load 41
+load 42
+load 43
+load 45
+load 46
+callsub alllaidtoargs_5
+store 47
+byte 0x151f7c75
+load 47
+itob
+concat
+log
+int 1
+return
+main_l14:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 24
+txna ApplicationArgs 2
+btoi
+store 25
+load 24
+load 25
+callsub mod_4
+store 26
+byte 0x151f7c75
+load 26
+itob
+concat
+log
+int 1
+return
+main_l15:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 18
+txna ApplicationArgs 2
+btoi
+store 19
+load 18
+load 19
+callsub div_3
+store 20
+byte 0x151f7c75
+load 20
+itob
+concat
+log
+int 1
+return
+main_l16:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 12
+txna ApplicationArgs 2
+btoi
+store 13
+load 12
+load 13
+callsub mul_2
+store 14
+byte 0x151f7c75
+load 14
+itob
+concat
+log
+int 1
+return
+main_l17:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 6
+txna ApplicationArgs 2
+btoi
+store 7
+load 6
+load 7
+callsub sub_1
+store 8
+byte 0x151f7c75
+load 8
+itob
+concat
+log
+int 1
+return
+main_l18:
+txn OnCompletion
+int NoOp
+==
+txn ApplicationID
+int 0
+!=
+&&
+assert
+txna ApplicationArgs 1
+btoi
+store 0
+txna ApplicationArgs 2
+btoi
+store 1
+load 0
+load 1
+callsub add_0
+store 2
+byte 0x151f7c75
+load 2
+itob
+concat
+log
+int 1
+return
+
+// add
+add_0:
+store 4
+store 3
+load 3
+load 4
++
+store 5
+load 5
+retsub
+
+// sub
+sub_1:
+store 10
+store 9
+load 9
+load 10
+-
+store 11
+load 11
+retsub
+
+// mul
+mul_2:
+store 16
+store 15
+load 15
+load 16
+*
+store 17
+load 17
+retsub
+
+// div
+div_3:
+store 22
+store 21
+load 21
+load 22
+/
+store 23
+load 23
+retsub
+
+// mod
+mod_4:
+store 28
+store 27
+load 27
+load 28
+%
+store 29
+load 29
+retsub
+
+// all_laid_to_args
+alllaidtoargs_5:
+store 63
+store 62
+store 61
+store 60
+store 59
+store 58
+store 57
+store 56
+store 55
+store 54
+store 53
+store 52
+store 51
+store 50
+store 49
+store 48
+load 48
+load 49
++
+load 50
++
+load 51
++
+load 52
++
+load 53
++
+load 54
++
+load 55
++
+load 56
++
+load 57
++
+load 58
++
+load 59
++
+load 60
++
+load 61
++
+load 62
++
+load 63
++
+store 64
+load 64
+retsub
+
+// empty_return_subroutine
+emptyreturnsubroutine_6:
+byte "appear in both approval and clear state"
+log
+retsub
+
+// log_1
+log1_7:
+int 1
+store 66
+load 66
+retsub
+
+// log_creation
+logcreation_8:
+byte "logging creation"
+len
+itob
+extract 6 0
+byte "logging creation"
+concat
+store 68
+load 68
+retsub""".strip()
+    assert actual_ap_without_oc_compiled == expected_ap_without_oc
+
+    expected_csp_without_oc = """#pragma version 6
+txna ApplicationArgs 0
+method "empty_return_subroutine()void"
+==
+bnz main_l6
+txna ApplicationArgs 0
+method "log_1()uint64"
+==
+bnz main_l5
+txna ApplicationArgs 0
+method "approve_if_odd(uint32)void"
+==
+bnz main_l4
+err
+main_l4:
+txn ApplicationID
+int 0
+!=
+assert
+txna ApplicationArgs 1
+int 0
+extract_uint32
+store 2
+load 2
+callsub approveifodd_2
+int 1
+return
+main_l5:
+txn ApplicationID
+int 0
+!=
+assert
+callsub log1_1
+store 1
+byte 0x151f7c75
+load 1
+itob
+concat
+log
+int 1
+return
+main_l6:
+txn ApplicationID
+int 0
+!=
+assert
+callsub emptyreturnsubroutine_0
+int 1
+return
+
+// empty_return_subroutine
+emptyreturnsubroutine_0:
+byte "appear in both approval and clear state"
+log
+retsub
+
+// log_1
+log1_1:
+int 1
+store 0
+load 0
+retsub
+
+// approve_if_odd
+approveifodd_2:
+store 3
+load 3
+int 2
+%
+bnz approveifodd_2_l2
+int 0
+return
+approveifodd_2_l2:
+int 1
+return""".strip()
+    assert actual_csp_without_oc_compiled == expected_csp_without_oc
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_().teal b/tests/integration/teal/roundtrip/app_roundtrip_().teal
index b59fbd275..6ce9ab897 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_().teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_().teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal
index 6344eb38b..2760bf66f 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal
index 56210aa2a..4023bb75d 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_2
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal
index 44f609078..87b114561 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal
@@ -4,7 +4,7 @@ store 3
 load 3
 callsub roundtripper_1
 store 2
-byte 0x151F7C75
+byte 0x151f7c75
 load 2
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal
index 3af93e1c1..591bfd847 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal
@@ -4,7 +4,7 @@ store 5
 load 5
 callsub roundtripper_1
 store 4
-byte 0x151F7C75
+byte 0x151f7c75
 load 4
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal
index f66d64459..7544bb298 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal
@@ -4,7 +4,7 @@ store 6
 load 6
 callsub roundtripper_2
 store 5
-byte 0x151F7C75
+byte 0x151f7c75
 load 5
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal
index 25e2ab3d7..110511e15 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal
@@ -4,7 +4,7 @@ store 6
 load 6
 callsub roundtripper_1
 store 5
-byte 0x151F7C75
+byte 0x151f7c75
 load 5
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal
index 7f8aabc87..6cd11c2f6 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal
@@ -4,7 +4,7 @@ store 4
 load 4
 callsub roundtripper_1
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal
index 2a4c05465..4e2b05add 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal
index cf613fa8c..f96dfc286 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal
@@ -4,7 +4,7 @@ store 4
 load 4
 callsub roundtripper_1
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal
index c9b93fc85..e0e119c13 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal
@@ -4,7 +4,7 @@ store 5
 load 5
 callsub roundtripper_2
 store 4
-byte 0x151F7C75
+byte 0x151f7c75
 load 4
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal
index 57d92721d..ec95bd989 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal
index 4af961953..0186d716b 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal
@@ -4,7 +4,7 @@ store 4
 load 4
 callsub roundtripper_1
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal
index e8f3179f1..e7f7c8437 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal
index 0194d3410..d0e0db70d 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal
@@ -4,7 +4,7 @@ store 4
 load 4
 callsub roundtripper_1
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal
index 2ef9adcec..d04d1c0ae 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal
index 7e33bb6ac..c47b327be 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal
@@ -4,7 +4,7 @@ store 4
 load 4
 callsub roundtripper_1
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal
index 2a4c05465..4e2b05add 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal
@@ -4,7 +4,7 @@ store 2
 load 2
 callsub roundtripper_1
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal
index 8f7115164..c42b55cfc 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal
@@ -4,10 +4,10 @@ store 4                     // 4 -> uint8|byte|bool
 load 4                      // [uint8|byte|bool]
 callsub roundtripper_1      // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool]
 store 3                     // 3 -> uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool
-byte 0x151F7C75             // [0x151F7C75]
-load 3                      // [0x151F7C75, uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool]
-concat                      // [0x151F7C75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool]
-log                         // log(0x151F7C75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool)
+byte 0x151f7c75             // [0x151f7c75]
+load 3                      // [0x151f7c75, uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool]
+concat                      // [0x151f7c75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool]
+log                         // log(0x151f7c75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool)
 int 1                       // [1]
 return                      // PASSED
 
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_address.teal b/tests/integration/teal/roundtrip/app_roundtrip_address.teal
index 0bc930d45..1a621ddc5 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_address.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_address.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal b/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal
index c406dcc4c..5667061f6 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_3
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool.teal
index bc91494eb..2f6390409 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool.teal
@@ -8,7 +8,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal
index 399bcf910..c79af3556 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal
index 0c8f9c611..04d6e6153 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_3
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal
index c0026b483..9d1fcc1eb 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal
index 213f6c9af..710b6a859 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal
index 3371ff0cd..9f38b5965 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal
index 8b4204e83..e15d49686 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_byte.teal b/tests/integration/teal/roundtrip/app_roundtrip_byte.teal
index 851809b8e..b3d294763 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_byte.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_byte.teal
@@ -6,7 +6,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal
index 5ae3edff2..6ab75fba8 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal
index 6350008aa..086b2b970 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal
index 779c6fc1d..a97b99d60 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal
index dc7d82530..81e911ee3 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal
@@ -6,7 +6,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal
index a46200138..ff6b2cb0a 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal
@@ -6,7 +6,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal
index 9a9169df4..c20d6fec7 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal
@@ -5,7 +5,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal
index 3252a6442..11a1cc180 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal
index 6afe66a2f..1f780d0f4 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal
index 213f6c9af..710b6a859 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal
index 70c2016c9..570b2cb20 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal
index d2ae51113..869f062b1 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal
@@ -4,7 +4,7 @@ store 1
 load 1
 callsub roundtripper_2
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal
index 851809b8e..b3d294763 100644
--- a/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal
+++ b/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal
@@ -6,7 +6,7 @@ store 1
 load 1
 callsub roundtripper_1
 store 0
-byte 0x151F7C75
+byte 0x151f7c75
 load 0
 concat
 log
diff --git a/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal b/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal
index 54a1a0a58..858410b51 100644
--- a/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal
+++ b/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal
@@ -1,7 +1,7 @@
 #pragma version 6
 callsub fn0arguint64ret_0
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 itob
 concat
diff --git a/tests/unit/teal/abi/app_fn_1arg_1ret.teal b/tests/unit/teal/abi/app_fn_1arg_1ret.teal
index 6ae354dac..f9d4574c7 100644
--- a/tests/unit/teal/abi/app_fn_1arg_1ret.teal
+++ b/tests/unit/teal/abi/app_fn_1arg_1ret.teal
@@ -5,7 +5,7 @@ store 3
 load 3
 callsub fn1arg1ret_0
 store 2
-byte 0x151F7C75
+byte 0x151f7c75
 load 2
 itob
 concat
diff --git a/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal b/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal
index bae9169fc..791abfc8a 100644
--- a/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal
+++ b/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal
@@ -2,7 +2,7 @@
 txna ApplicationArgs 0
 callsub fn1ttarguint64ret_0
 store 2
-byte 0x151F7C75
+byte 0x151f7c75
 load 2
 itob
 concat
diff --git a/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal b/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal
index ed0ce47e3..51c058bd6 100644
--- a/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal
+++ b/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal
@@ -8,7 +8,7 @@ load 4
 int 5
 callsub fn2mixedarg1ret_0
 store 3
-byte 0x151F7C75
+byte 0x151f7c75
 load 3
 itob
 concat
diff --git a/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal b/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal
index a19e893be..3d844d44d 100644
--- a/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal
+++ b/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal
@@ -4,7 +4,7 @@ store 0                     // 0: x
 load 0                      // [x]
 callsub abisum_0
 store 1
-byte 0x151F7C75
+byte 0x151f7c75
 load 1
 itob
 concat
@@ -20,8 +20,8 @@ store 3                     // 3: 0
 int 0                       // [0]
 store 4                     // 4: 0
 abisum_0_l1:                // []
-load 4                      
-load 2                      
+load 4
+load 2
 int 0                       // [0, x, 0]
 extract_uint16              // [0, len(x)]
 store 6                     // 6: len(x)