Repository: vyperlang/vyper

Issue:
VIP: Change binary literal format
This is the suggested template for new VIPs.

## Preamble

    VIP: 732
    Title: Change binary literal format 
    Author: jamesray1
    Type: Standard Track
    Status: Draft
    Created: 2018-03-30
    
## Simple Summary
Change the byte string literal format type to use python's `0b` format.

## Abstract
Change bytes variable assignment from `someBytes = "00010110"` to `someBytes = 0b00010110` and also support hexadecimal format and octal format, but not decimal format.

## Motivation
Python has a know literal type for representing bytes, it stands to reason to use it.
```
>>> a = 0b111
>>> a
7
```

## Specification
Before:
```
someBytes: bytes[1] = "00010110"
```
After:
```
someBytes: bytes[1] = 0b00010110
```

## Backwards Compatibility
Not backwards compatible.

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)


Discussion:
I'm in favor.
In favor

Resolving patch:
diff --git a/vyper/functions/signature.py b/vyper/functions/signature.py
--- a/vyper/functions/signature.py
+++ b/vyper/functions/signature.py
@@ -1,7 +1,7 @@
 import ast
 
 from vyper.parser.parser_utils import (
-    get_original_if_0x_prefixed,
+    get_original_if_0_prefixed,
 )
 from vyper.exceptions import (
     TypeMismatchException,
@@ -35,10 +35,10 @@ def process_arg(index, arg, expected_arg_typelist, function_name, context):
     vsub = None
     for expected_arg in expected_arg_typelist:
         if expected_arg == 'num_literal':
-            if isinstance(arg, ast.Num) and get_original_if_0x_prefixed(arg, context) is None:
+            if isinstance(arg, ast.Num) and get_original_if_0_prefixed(arg, context) is None:
                 return arg.n
         elif expected_arg == 'str_literal':
-            if isinstance(arg, ast.Str) and get_original_if_0x_prefixed(arg, context) is None:
+            if isinstance(arg, ast.Str) and get_original_if_0_prefixed(arg, context) is None:
                 bytez = b''
                 for c in arg.s:
                     if ord(c) >= 256:
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -7,13 +7,13 @@
     StructureException,
     TypeMismatchException,
     VariableDeclarationException,
-    ParserException,
+    ParserException
 )
 from .parser_utils import LLLnode
 from .parser_utils import (
     getpos,
     unwrap_location,
-    get_original_if_0x_prefixed,
+    get_original_if_0_prefixed,
     get_number_as_fraction,
     add_variable_offset,
 )
@@ -78,7 +78,7 @@ def get_expr(self):
         return self.expr
 
     def number(self):
-        orignum = get_original_if_0x_prefixed(self.expr, self.context)
+        orignum = get_original_if_0_prefixed(self.expr, self.context)
 
         if orignum is None and isinstance(self.expr.n, int):
 
@@ -98,6 +98,25 @@ def number(self):
             if DECIMAL_DIVISOR % den:
                 raise InvalidLiteralException("Too many decimal places: " + numstring, self.expr)
             return LLLnode.from_list(num * DECIMAL_DIVISOR // den, typ=BaseType('decimal', None), pos=getpos(self.expr))
+        # Binary literal.
+        elif orignum[:2] == '0b':
+            str_val = orignum[2:]
+            total_bits = len(orignum[2:])
+            total_bits = total_bits if total_bits % 8 == 0 else total_bits + 8 - (total_bits % 8)  # ceil8 to get byte length.
+            if total_bits != len(orignum[2:]):  # add necessary zero padding.
+                pad_len = total_bits - len(orignum[2:])
+                str_val = pad_len * '0' + str_val
+            byte_len = int(total_bits / 8)
+            placeholder = self.context.new_placeholder(ByteArrayType(byte_len))
+            seq = []
+            seq.append(['mstore', placeholder, byte_len])
+            for i in range(0, total_bits, 256):
+                section = str_val[i:i + 256]
+                int_val = int(section, 2) << (256 - len(section))  # bytes are right padded.
+                seq.append(
+                    ['mstore', ['add', placeholder, i + 32], int_val])
+            return LLLnode.from_list(['seq'] + seq + [placeholder],
+                typ=ByteArrayType(byte_len), location='memory', pos=getpos(self.expr), annotation='Create ByteArray (Binary literal): %s' % str_val)
         elif len(orignum) == 42:
             if checksum_encode(orignum) != orignum:
                 raise InvalidLiteralException("Address checksum mismatch. If you are sure this is the "
@@ -450,7 +469,13 @@ def build_in_comparator(self):
     def compare(self):
         left = Expr.parse_value_expr(self.expr.left, self.context)
         right = Expr.parse_value_expr(self.expr.comparators[0], self.context)
-        if isinstance(self.expr.ops[0], ast.In) and \
+
+        if isinstance(left.typ, ByteArrayType) and isinstance(right.typ, ByteArrayType):
+            if left.typ.maxlen != right.typ.maxlen:
+                raise TypeMismatchException('Can only compare bytes of the same length', self.expr)
+            if left.typ.maxlen > 32 or right.typ.maxlen > 32:
+                raise ParserException('Can only compare bytes of length shorter than 32 bytes', self.expr)
+        elif isinstance(self.expr.ops[0], ast.In) and \
            isinstance(right.typ, ListType):
             if not are_units_compatible(left.typ, right.typ.subtype) and not are_units_compatible(right.typ.subtype, left.typ):
                 raise TypeMismatchException("Can't use IN comparison with different types!", self.expr)
@@ -458,6 +483,7 @@ def compare(self):
         else:
             if not are_units_compatible(left.typ, right.typ) and not are_units_compatible(right.typ, left.typ):
                 raise TypeMismatchException("Can't compare values with different units!", self.expr)
+
         if len(self.expr.ops) != 1:
             raise StructureException("Cannot have a comparison with more than two elements", self.expr)
         if isinstance(self.expr.ops[0], ast.Gt):
@@ -474,6 +500,22 @@ def compare(self):
             op = 'ne'
         else:
             raise Exception("Unsupported comparison operator")
+
+        # Compare (limited to 32) byte arrays.
+        if isinstance(left.typ, ByteArrayType) and isinstance(left.typ, ByteArrayType):
+            left = Expr(self.expr.left, self.context).lll_node
+            right = Expr(self.expr.comparators[0], self.context).lll_node
+
+            def load_bytearray(side):
+                if side.location == 'memory':
+                    return ['mload', ['add', 32, side]]
+                elif side.location == 'storage':
+                    return ['sload', ['add', 1, ['sha3_32', side]]]
+
+            return LLLnode.from_list(
+                [op, load_bytearray(left), load_bytearray(right)], typ='bool', pos=getpos(self.expr))
+
+        # Compare other types.
         if not is_numeric_type(left.typ) or not is_numeric_type(right.typ):
             if op not in ('eq', 'ne'):
                 raise TypeMismatchException("Invalid type for comparison op", self.expr)
diff --git a/vyper/parser/parser_utils.py b/vyper/parser/parser_utils.py
--- a/vyper/parser/parser_utils.py
+++ b/vyper/parser/parser_utils.py
@@ -51,6 +51,7 @@ def __init__(self, value, args=None, typ=None, location=None, pos=None, annotati
         self.annotation = annotation
         self.mutable = mutable
         self.add_gas_estimate = add_gas_estimate
+
         # Determine this node's valency (1 if it pushes a value on the stack,
         # 0 otherwise) and checks to make sure the number and valencies of
         # children are correct. Also, find an upper bound on gas consumption
@@ -238,15 +239,24 @@ def get_number_as_fraction(expr, context):
     return context_slice[:t], top, bottom
 
 
-# Is a number of decimal form (e.g. 65281) or 0x form (e.g. 0xff01)
-def get_original_if_0x_prefixed(expr, context):
+# Is a number of decimal form (e.g. 65281) or 0x form (e.g. 0xff01) or 0b binary form (e.g. 0b0001)
+def get_original_if_0_prefixed(expr, context):
     context_slice = context.origcode.splitlines()[expr.lineno - 1][expr.col_offset:]
-    if context_slice[:2] != '0x':
+    type_prefix = context_slice[:2]
+
+    if type_prefix not in ('0x', '0b'):
         return None
-    t = 0
-    while t + 2 < len(context_slice) and context_slice[t + 2] in '0123456789abcdefABCDEF':
-        t += 1
-    return context_slice[:t + 2]
+
+    if type_prefix == '0x':
+        t = 0
+        while t + 2 < len(context_slice) and context_slice[t + 2] in '0123456789abcdefABCDEF':
+            t += 1
+        return context_slice[:t + 2]
+    elif type_prefix == '0b':
+        t = 0
+        while t + 2 < len(context_slice) and context_slice[t + 2] in '01':
+            t += 1
+        return context_slice[:t + 2]
 
 
 # Copies byte array


Test patch:
diff --git a/tests/parser/types/test_bytes.py b/tests/parser/types/test_bytes.py
--- a/tests/parser/types/test_bytes.py
+++ b/tests/parser/types/test_bytes.py
@@ -1,5 +1,6 @@
 import pytest
 from ethereum.tools import tester
+from vyper.exceptions import ParserException, TypeMismatchException
 
 
 def test_test_bytes(get_contract_with_gas_estimation):
@@ -189,3 +190,62 @@ def bar_storage() -> int128:
     assert_tx_failed(lambda: c.foo(b"\x01" * 33))
     assert c.bar_storage() == 97
     print('Passed bytes_to_num tests')
+
+
+def test_binary_literal(get_contract_with_gas_estimation):
+    bytes_to_num_code = """
+r: bytes[1]
+
+@public
+def get(a: bytes[1]) -> bytes[2]:
+    return concat(a, 0b0001)
+
+@public
+def getsome() -> bytes[1]:
+    return 0b1110
+
+@public
+def testsome(a: bytes[1]) -> bool:
+    return a == 0b1100001
+
+@public
+def testsome_storage(y: bytes[1]) -> bool:
+    self.r = 0b1100001
+    return self.r == y
+    """
+
+    c = get_contract_with_gas_estimation(bytes_to_num_code)
+
+    assert c.getsome() == b'\x0e'
+    assert c.testsome('a')
+    assert c.testsome('\x61')
+    assert c.testsome(0b1100001.to_bytes(1, 'big'))
+    assert not c.testsome('b')
+    assert c.testsome_storage('a')
+    assert not c.testsome_storage('x')
+
+
+def test_bytes_comparison_fail_size_mismatch(assert_compile_failed, get_contract_with_gas_estimation):
+    code = """
+@public
+def get(a: bytes[1]) -> bool:
+    b: bytes[2] = 'ab'
+    return a == b
+    """
+    assert_compile_failed(
+        lambda: get_contract_with_gas_estimation(code),
+        TypeMismatchException
+    )
+
+
+def test_bytes_comparison_fail_too_large(assert_compile_failed, get_contract_with_gas_estimation):
+    code = """
+@public
+def get(a: bytes[100]) -> bool:
+    b: bytes[100] = 'ab'
+    return a == b
+    """
+    assert_compile_failed(
+        lambda: get_contract_with_gas_estimation(code),
+        ParserException
+    )