Skip to content

Commit

Permalink
Handle integer overflow/underflow for non-64-bit integers (FuelLabs#4707
Browse files Browse the repository at this point in the history
)

fixes FuelLabs#4646

## Description

This PR fixes the linked issue by introducing overflow/underflow checks
in the `Add`, `Mul`, `Sub` stdlib traits implementations for the `u8`,
`u16` and `u32` types.

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
anton-trunov authored Jun 29, 2023
1 parent f246d97 commit 0263c0d
Show file tree
Hide file tree
Showing 103 changed files with 1,039 additions and 64 deletions.
7 changes: 7 additions & 0 deletions docs/book/src/basics/built_in_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ Numbers can be declared with binary syntax, hexadecimal syntax, base-10 syntax,
<!-- This section should explain the default numeric type in Sway -->
<!-- default_num:example:start -->
The default numeric type is `u64`. The FuelVM's word size is 64 bits, and the cases where using a smaller numeric type saves space are minimal.

If a 64-bit arithmetic operation produces an overflow or an underflow,
computation gets reverted automatically by FuelVM.

8/16/32-bit arithmetic operations are emulated using their 64-bit analogues with
additional overflow/underflow checks inserted, which generally results in
somewhat higher gas consumption.
<!-- default_num:example:end -->

## Boolean Type
Expand Down
21 changes: 21 additions & 0 deletions docs/book/src/basics/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ Constants are similar to variables; however, there are a few differences:
const ID: u32 = 0;
```

Constant initializer expressions can be quite complex, but they cannot use, for
instance, assembly instructions, storage access, mutable variables, loops and
`return` statements. Although, function calls, primitive types and compound data
structures are perfectly fine to use:

```sway
fn bool_to_num(b: bool) -> u64 {
if b {
1
} else {
0
}
}
fn arr_wrapper(a: u64, b: u64, c: u64) -> [u64; 3] {
[a, b, c]
}
const ARR2 = arr_wrapper(bool_to_num(1) + 42, 2, 3);
```

## Associated Constants

<!-- This section should explain what associated constants are -->
Expand Down
113 changes: 83 additions & 30 deletions sway-core/src/ir_generation/const_eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,17 +266,8 @@ fn const_eval_typed_expr(
known_consts.push(name.clone(), cval);
}

// TODO: Handle more than one statement in the block.
let function_decl = lookup.engines.de().get_function(fn_ref);
if function_decl.body.contents.len() > 1 {
return Ok(None);
}
let body_contents_opt = function_decl.body.contents.last();
let res = if let Some(first_expr) = body_contents_opt {
const_eval_typed_ast_node(lookup, known_consts, first_expr)?
} else {
None
};
let res = const_eval_codeblock(lookup, known_consts, &function_decl.body)?;
for (name, _) in arguments {
known_consts.pop(name);
}
Expand Down Expand Up @@ -484,18 +475,47 @@ fn const_eval_typed_expr(
}) => fields.get(*elem_to_access_num).cloned(),
_ => None,
},
ty::TyExpressionVariant::Return(exp) => const_eval_typed_expr(lookup, known_consts, exp)?,
// we could allow non-local control flow in pure functions, but it would
// require some more work and at this point it's not clear if it is too useful
// for constant initializers -- the user can always refactor their pure functions
// to not use the return statement
ty::TyExpressionVariant::Return(_exp) => None,
ty::TyExpressionVariant::MatchExp { desugared, .. } => {
const_eval_typed_expr(lookup, known_consts, desugared)?
}
ty::TyExpressionVariant::IntrinsicFunction(kind) => {
const_eval_intrinsic(lookup, known_consts, kind)?
}
ty::TyExpressionVariant::IfExp {
condition,
then,
r#else,
} => {
match const_eval_typed_expr(lookup, known_consts, condition)? {
Some(Constant {
value: ConstantValue::Bool(cond),
..
}) => {
if cond {
const_eval_typed_expr(lookup, known_consts, then)?
} else if let Some(r#else) = r#else {
const_eval_typed_expr(lookup, known_consts, r#else)?
} else {
// missing 'else' branch:
// we probably don't really care about evaluating
// const expressions of the unit type
None
}
}
_ => None,
}
}
ty::TyExpressionVariant::CodeBlock(codeblock) => {
const_eval_codeblock(lookup, known_consts, codeblock)?
}
ty::TyExpressionVariant::ArrayIndex { .. }
| ty::TyExpressionVariant::CodeBlock(_)
| ty::TyExpressionVariant::Reassignment(_)
| ty::TyExpressionVariant::FunctionParameter
| ty::TyExpressionVariant::IfExp { .. }
| ty::TyExpressionVariant::AsmExpression { .. }
| ty::TyExpressionVariant::LazyOperator { .. }
| ty::TyExpressionVariant::AbiCast { .. }
Expand All @@ -509,6 +529,56 @@ fn const_eval_typed_expr(
})
}

// the (constant) value of a codeblock is essentially it's last expression if there is one
// or if it makes sense as the last expression, e.g. a dangling let-expression in a codeblock
// would be an evaluation error
fn const_eval_codeblock(
lookup: &mut LookupEnv,
known_consts: &mut MappedStack<Ident, Constant>,
codeblock: &ty::TyCodeBlock,
) -> Result<Option<Constant>, CompileError> {
// the current result
let mut res_const = None;
// keep track of new bindings for this codeblock
let mut bindings: Vec<_> = vec![];

for ast_node in &codeblock.contents {
match &ast_node.content {
ty::TyAstNodeContent::Declaration(ty::TyDecl::VariableDecl(var_decl)) => {
let rhs_opt = const_eval_typed_expr(lookup, known_consts, &var_decl.body)?;
if let Some(rhs) = rhs_opt {
known_consts.push(var_decl.name.clone(), rhs);
bindings.push(var_decl.name.clone());
}
res_const = None
}
ty::TyAstNodeContent::Declaration(ty::TyDecl::ConstantDecl(const_decl)) => {
let ty_const_decl = lookup.engines.de().get_constant(&const_decl.decl_id);
if let Some(const_expr) = ty_const_decl.value {
if let Some(constant) =
const_eval_typed_expr(lookup, known_consts, &const_expr)?
{
known_consts.push(const_decl.name.clone(), constant);
bindings.push(const_decl.name.clone());
}
}
res_const = None
}
ty::TyAstNodeContent::Declaration(_) => res_const = None,
ty::TyAstNodeContent::Expression(e)
| ty::TyAstNodeContent::ImplicitReturnExpression(e) => {
res_const = const_eval_typed_expr(lookup, known_consts, e)?
}
ty::TyAstNodeContent::SideEffect(_) => res_const = None,
}
}
// remove introduced vars/consts from scope at the end of the codeblock
for name in bindings {
known_consts.pop(&name)
}
Ok(res_const)
}

fn const_eval_intrinsic(
lookup: &mut LookupEnv,
known_consts: &mut MappedStack<Ident, Constant>,
Expand Down Expand Up @@ -675,20 +745,3 @@ fn const_eval_intrinsic(
| sway_ast::Intrinsic::Smo => Ok(None),
}
}

fn const_eval_typed_ast_node(
lookup: &mut LookupEnv,
known_consts: &mut MappedStack<Ident, Constant>,
expr: &ty::TyAstNode,
) -> Result<Option<Constant>, CompileError> {
match &expr.content {
ty::TyAstNodeContent::Declaration(_) => {
// TODO: add the binding to known_consts (if it's a const) and proceed.
Ok(None)
}
ty::TyAstNodeContent::Expression(e) | ty::TyAstNodeContent::ImplicitReturnExpression(e) => {
const_eval_typed_expr(lookup, known_consts, e)
}
ty::TyAstNodeContent::SideEffect(_) => Ok(None),
}
}
66 changes: 57 additions & 9 deletions sway-lib-core/src/ops.sw
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,41 @@ impl Add for u64 {
}
}

// Emulate overflowing arithmetic for non-64-bit integer types
impl Add for u32 {
fn add(self, other: Self) -> Self {
__add(self, other)
// any non-64-bit value is compiled to a u64 value under-the-hood
// constants (like Self::max() below) are also automatically promoted to u64
let res = __add(self, other);
if __gt(res, Self::max()) {
// integer overflow
__revert(0)
} else {
// no overflow
res
}
}
}

impl Add for u16 {
fn add(self, other: Self) -> Self {
__add(self, other)
let res = __add(self, other);
if __gt(res, Self::max()) {
__revert(0)
} else {
res
}
}
}

impl Add for u8 {
fn add(self, other: Self) -> Self {
__add(self, other)
let res = __add(self, other);
if __gt(res, Self::max()) {
__revert(0)
} else {
res
}
}
}

Expand All @@ -40,6 +60,8 @@ impl Subtract for u64 {
}
}

// unlike addition, underflowing subtraction does not need special treatment
// because VM handles underflow
impl Subtract for u32 {
fn subtract(self, other: Self) -> Self {
__sub(self, other)
Expand Down Expand Up @@ -68,21 +90,41 @@ impl Multiply for u64 {
}
}

// Emulate overflowing arithmetic for non-64-bit integer types
impl Multiply for u32 {
fn multiply(self, other: Self) -> Self {
__mul(self, other)
// any non-64-bit value is compiled to a u64 value under-the-hood
// constants (like Self::max() below) are also automatically promoted to u64
let res = __mul(self, other);
if __gt(res, Self::max()) {
// integer overflow
__revert(0)
} else {
// no overflow
res
}
}
}

impl Multiply for u16 {
fn multiply(self, other: Self) -> Self {
__mul(self, other)
let res = __mul(self, other);
if __gt(res, Self::max()) {
__revert(0)
} else {
res
}
}
}

impl Multiply for u8 {
fn multiply(self, other: Self) -> Self {
__mul(self, other)
let res = __mul(self, other);
if __gt(res, Self::max()) {
__revert(0)
} else {
res
}
}
}

Expand All @@ -96,6 +138,10 @@ impl Divide for u64 {
}
}

// division for unsigned integers cannot overflow,
// but if signed integers are ever introduced,
// overflow needs to be handled, since
// Self::max() / -1 overflows
impl Divide for u32 {
fn divide(self, other: Self) -> Self {
__div(self, other)
Expand Down Expand Up @@ -482,7 +528,9 @@ impl Shift for u64 {

impl Shift for u32 {
fn lsh(self, other: u64) -> Self {
__lsh(self, other)
// any non-64-bit value is compiled to a u64 value under-the-hood
// so we need to clear upper bits here
__and(__lsh(self, other), Self::max())
}
fn rsh(self, other: u64) -> Self {
__rsh(self, other)
Expand All @@ -491,7 +539,7 @@ impl Shift for u32 {

impl Shift for u16 {
fn lsh(self, other: u64) -> Self {
__lsh(self, other)
__and(__lsh(self, other), Self::max())
}
fn rsh(self, other: u64) -> Self {
__rsh(self, other)
Expand All @@ -500,7 +548,7 @@ impl Shift for u16 {

impl Shift for u8 {
fn lsh(self, other: u64) -> Self {
__lsh(self, other)
__and(__lsh(self, other), Self::max())
}
fn rsh(self, other: u64) -> Self {
__rsh(self, other)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = 'core'
source = 'path+from-root-0EAD15BE42537FD3'

[[package]]
name = 'std'
source = 'path+from-root-0EAD15BE42537FD3'
dependencies = ['core']

[[package]]
name = 'u16_add_const_eval_overflow'
source = 'member'
dependencies = ['std']
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "u16_add_const_eval_overflow"

[dependencies]
std = { path = "../../../../../../../sway-lib-std" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
script;

const RESULT: u16 = u16::max() + 1;

fn main() -> bool {
log(RESULT);

true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
category = "fail"

# check: $()Could not evaluate initializer to a const declaration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[[package]]
name = 'core'
source = 'path+from-root-DD034BDFC1AFC889'

[[package]]
name = 'std'
source = 'path+from-root-DD034BDFC1AFC889'
dependencies = ['core']

[[package]]
name = 'u16_add_overflow'
source = 'member'
dependencies = ['std']
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "u16_add_overflow"

[dependencies]
std = { path = "../../../../../../../sway-lib-std" }
Loading

0 comments on commit 0263c0d

Please sign in to comment.