From d7993ec147328d7b98e68793a4f6f60c580a9805 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 4 Mar 2024 17:56:41 -0800 Subject: [PATCH 1/3] feat[lang]: allow downcasting of bytestrings allow conversion from Bytes/String types to shorter length types, e.g. convert `Bytes[20]` to `Bytes[19]` this will become important when we want to allow generic bytestrings inside the type system (`Bytes[...]`) which can only be user-instantiated by converting to a known length. --- vyper/builtins/_convert.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/vyper/builtins/_convert.py b/vyper/builtins/_convert.py index 156bee418e..eedec8a5cc 100644 --- a/vyper/builtins/_convert.py +++ b/vyper/builtins/_convert.py @@ -10,6 +10,7 @@ bytes_data_ptr, clamp, clamp_basetype, + clamp_bytestring, clamp_le, get_bytearray_length, int_clamp, @@ -422,23 +423,25 @@ def to_address(expr, arg, out_typ): return IRnode.from_list(ret, out_typ) -# question: should we allow bytesM -> String? -@_input_types(BytesT) -def to_string(expr, arg, out_typ): - _check_bytes(expr, arg, out_typ, out_typ.maxlen) - +def _cast_bytestring(expr, arg, out_typ): + if isinstance(arg.typ, out_typ.__class__) and out_typ.maxlen <= arg.typ.maxlen: + _FAIL(arg.typ, out_typ, expr) + ret = ["seq"] + if out_typ.maxlen is None or out_typ.maxlen > arg.maxlen: + ret.append(clamp_bytestring(arg)) # NOTE: this is a pointer cast return IRnode.from_list(arg, typ=out_typ) -@_input_types(StringT) -def to_bytes(expr, arg, out_typ): - _check_bytes(expr, arg, out_typ, out_typ.maxlen) +# question: should we allow bytesM -> String? +@_input_types(BytesT, StringT) +def to_string(expr, arg, out_typ): + return _cast_bytestring(expr, arg, out_typ) - # TODO: more casts - # NOTE: this is a pointer cast - return IRnode.from_list(arg, typ=out_typ) +@_input_types(StringT, BytesT) +def to_bytes(expr, arg, out_typ): + return _cast_bytestring(expr, arg, out_typ) @_input_types(IntegerT) From 93e53c1af63581e56c8f6876c4174e4b79886a72 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 4 Mar 2024 18:05:53 -0800 Subject: [PATCH 2/3] fix direction of some comparisons --- vyper/builtins/_convert.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/vyper/builtins/_convert.py b/vyper/builtins/_convert.py index eedec8a5cc..cb9a86884f 100644 --- a/vyper/builtins/_convert.py +++ b/vyper/builtins/_convert.py @@ -10,7 +10,6 @@ bytes_data_ptr, clamp, clamp_basetype, - clamp_bytestring, clamp_le, get_bytearray_length, int_clamp, @@ -424,13 +423,15 @@ def to_address(expr, arg, out_typ): def _cast_bytestring(expr, arg, out_typ): - if isinstance(arg.typ, out_typ.__class__) and out_typ.maxlen <= arg.typ.maxlen: + # can't convert Bytes[20] to Bytes[21] + if isinstance(arg.typ, out_typ.__class__) and arg.typ.maxlen <= out_typ.maxlen: _FAIL(arg.typ, out_typ, expr) ret = ["seq"] - if out_typ.maxlen is None or out_typ.maxlen > arg.maxlen: - ret.append(clamp_bytestring(arg)) + if out_typ.maxlen < arg.typ.maxlen: + ret.append(["assert", ["le", get_bytearray_length(arg), out_typ.maxlen]]) + ret.append(arg) # NOTE: this is a pointer cast - return IRnode.from_list(arg, typ=out_typ) + return IRnode.from_list(ret, typ=out_typ, location=arg.location, encoding=arg.encoding) # question: should we allow bytesM -> String? From b2e62a2299f7a7cce00cf87deb291769f56caf09 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 6 Mar 2024 08:08:25 -0800 Subject: [PATCH 3/3] fix existing tests, add tests for new functionality, add compile-time check --- .../builtins/codegen/test_convert.py | 61 +++++++++++++++++-- vyper/builtins/_convert.py | 6 +- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/tests/functional/builtins/codegen/test_convert.py b/tests/functional/builtins/codegen/test_convert.py index 559e1448ef..c1063bd0e4 100644 --- a/tests/functional/builtins/codegen/test_convert.py +++ b/tests/functional/builtins/codegen/test_convert.py @@ -8,6 +8,7 @@ import eth.codecs.abi.exceptions import pytest +from vyper.compiler import compile_code from vyper.exceptions import InvalidLiteral, InvalidType, TypeMismatch from vyper.semantics.types import AddressT, BoolT, BytesM_T, BytesT, DecimalT, IntegerT, StringT from vyper.semantics.types.shortcuts import BYTES20_T, BYTES32_T, UINT, UINT160_T, UINT256_T @@ -560,14 +561,15 @@ def foo(x: {i_typ}) -> {o_typ}: assert_compile_failed(lambda: get_contract(code), TypeMismatch) -@pytest.mark.parametrize("typ", sorted(TEST_TYPES)) -def test_bytes_too_large_cases(get_contract, assert_compile_failed, typ): +@pytest.mark.parametrize("typ", sorted(BASE_TYPES)) +def test_bytes_too_large_cases(typ): code_1 = f""" @external def foo(x: Bytes[33]) -> {typ}: return convert(x, {typ}) """ - assert_compile_failed(lambda: get_contract(code_1), TypeMismatch) + with pytest.raises(TypeMismatch): + compile_code(code_1) bytes_33 = b"1" * 33 code_2 = f""" @@ -575,8 +577,59 @@ def foo(x: Bytes[33]) -> {typ}: def foo() -> {typ}: return convert({bytes_33}, {typ}) """ + with pytest.raises(TypeMismatch): + compile_code(code_2) - assert_compile_failed(lambda: get_contract(code_2, TypeMismatch)) + +@pytest.mark.parametrize("cls1,cls2", itertools.product((StringT, BytesT), (StringT, BytesT))) +def test_bytestring_conversions(cls1, cls2, get_contract, tx_failed): + typ1 = cls1(33) + typ2 = cls2(32) + + def bytestring(cls, string): + if cls == BytesT: + return string.encode("utf-8") + return string + + code_1 = f""" +@external +def foo(x: {typ1}) -> {typ2}: + return convert(x, {typ2}) + """ + c = get_contract(code_1) + + for i in range(33): # inclusive 32 + s = "1" * i + arg = bytestring(cls1, s) + out = bytestring(cls2, s) + assert c.foo(arg) == out + + with tx_failed(): + # TODO: sanity check it is convert which is reverting, not arg clamping + c.foo(bytestring(cls1, "1" * 33)) + + code_2_template = """ +@external +def foo() -> {typ}: + return convert({arg}, {typ}) + """ + + # test literals + for i in range(33): # inclusive 32 + s = "1" * i + arg = bytestring(cls1, s) + out = bytestring(cls2, s) + code = code_2_template.format(typ=typ2, arg=repr(arg)) + if cls1 == cls2: # ex.: can't convert "" to String[32] + with pytest.raises(InvalidType): + compile_code(code) + else: + c = get_contract(code) + assert c.foo() == out + + failing_code = code_2_template.format(typ=typ2, arg=bytestring(cls1, "1" * 33)) + with pytest.raises(TypeMismatch): + compile_code(failing_code) @pytest.mark.parametrize("n", range(1, 33)) diff --git a/vyper/builtins/_convert.py b/vyper/builtins/_convert.py index cb9a86884f..8f5f4c03e2 100644 --- a/vyper/builtins/_convert.py +++ b/vyper/builtins/_convert.py @@ -423,9 +423,13 @@ def to_address(expr, arg, out_typ): def _cast_bytestring(expr, arg, out_typ): - # can't convert Bytes[20] to Bytes[21] + # ban converting Bytes[20] to Bytes[21] if isinstance(arg.typ, out_typ.__class__) and arg.typ.maxlen <= out_typ.maxlen: _FAIL(arg.typ, out_typ, expr) + # can't downcast literals with known length (e.g. b"abc" to Bytes[2]) + if isinstance(expr, vy_ast.Constant) and arg.typ.maxlen > out_typ.maxlen: + _FAIL(arg.typ, out_typ, expr) + ret = ["seq"] if out_typ.maxlen < arg.typ.maxlen: ret.append(["assert", ["le", get_bytearray_length(arg), out_typ.maxlen]])