Skip to content

Commit

Permalink
Add docs on clever errors
Browse files Browse the repository at this point in the history
  • Loading branch information
tzakian committed Oct 30, 2024
1 parent 8628621 commit 6f871c0
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
1 change: 1 addition & 0 deletions reference/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Local Variables and Scopes](variables.md)
- [Equality](equality.md)
- [Abort and Assert](abort-and-assert.md)
- [Clever Errors](abort-and-assert/clever-errors.md)
- [Control Flow](control-flow.md)
- [Conditional Expressions](control-flow/conditionals.md)
- [Loops](control-flow/loops.md)
Expand Down
230 changes: 230 additions & 0 deletions reference/src/abort-and-assert/clever-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Clever Errors

Clever errors are a feature that allows for more informative error messages when an assertion fails
or an abort is raised. They are a source feature and compile to a `u64` abort code value that
contains the information needed to access the line number, constant name, and constant value given
the clever error code and the module that the clever error constant was declared in. Because of
this, post-processing is required to go from the `u64` abort code value to a human-readable error
message. This post-processing is automatically performed by the Sui GraphQL server, as well as the
Sui CLI. If you want to manually decode a clever abort code, you can use the process outlined in
[Inflating Clever Abort Codes](#inflating-clever-abort-codes) to do so.

> Clever errors include source line information amongst other data. Because of this their value may
> change due to any changes in the source file (e.g., due to auto-formatting, adding a new module
> member, or adding a newline).
## Clever Abort Codes

Clever abort codes allow you to use non-u64 constants as abort codes as long as the constants are
annotated with the `#[error]` attribute. They can be used both in assertions, and as codes to
`abort`.

```move
module 0x42::a_module;
#[error]
const EIsThree: vector<u8> = b"The value is three";
// Will abort with `EIsThree` if `x` is 3
public fun double_except_three(x: u64): u64 {
assert!(x != 3, EIsThree);
x * x
}
// Will always abort with `EIsThree`
public fun clever_abort() {
abort EIsThree
}
```

In this example, the `EIsThree` constant is a `vector<u8>`, which is not a `u64`. However, the
`#[error]` attribute allows the constant to be used as an abort code, and will at runtime produce a
`u64` abort code value that holds:

1. A set tag-bit that indicates that the abort code is a clever abort code.
2. The line number of where the abort occured in the source file (e.g., 7).
3. The index in the module's identifier table for the constant's name (e.g., `EIsThree`).
4. The index of the constant's value in the module's constant table (e.g., `b"The value is three"`).

In hex, if `double_except_three(3)` is called, it will abort with a `u64` abort code as follows:

```
0x8000_7000_1000_0000
^ ^ ^ ^
| | | |
| | | |
| | | +-- Constant value index = 0 (b"The value is three")
| | +-- Constant name index = 1 (EIsThree)
| +-- Line number = 7 (line of the assertion)
+-- Tag bit = 0b1000_0000_0000_0000
```

And could be rendered as a human-readable error message as (e.g.)

```
Error from '0x42::a_module::double_except_three' (line 7), abort 'EIsThree': "The value is three"
```

The exact formatting of this message may vary depending on the tooling used to decode the clever
error however all of the information needed to generate a human-readable error message like the
above is present in the `u64` abort code when coupled with the module where the error occurred.

> Clever abort code values do _not_ need to be a `vector<u8>` -- it can be any valid constant type
> in Move.
## Assertions with no Abort Codes

Assertions, and `abort` statements without an abort code will automatically derive an abort code
from the source line number and will be encoded in the clever error format with the constant name
and constant value information will be filled with sentinel values of `0xffff` each. E.g.,

```move
module 0x42::a_module;
#[test]
fun assert_false(x: bool) {
assert!(false);
}
#[test]
fun abort_no_code() {
abort
}
```

Both of these will produce a `u64` abort code value that holds:

1. A set tag-bit that indicates that the abort code is a clever abort code.
2. The line number of where the abort occured in the source file (e.g., 6).
3. A sentinel value of `0xffff` for the index into the module's identifier table for the constant's
name.
4. A sentinel value of `0xffff` for the index of the constant's value in the module's constant
table.

In hex, if `assert_false(3)` is called, it will abort with a `u64` abort code as follows:

```
0x8000_4000_ffff_ffff
^ ^ ^ ^
| | | |
| | | |
| | | +-- Constant value index = 0xffff (sentinel value)
| | +-- Constant name index = 0xffff (sentinel value)
| +-- Line number = 4 (linke of the assertion)
+-- Tag bit = 0b1000_0000_0000_0000
```

## Clever Errors and Macros

The line number information in clever abort codes are derived from the source file at the location
where the abort occurs. In particular, for a function this will be the line number within in the
function, however for macros, this will be the location where the macro is invoked. This can be
quite useful when writing macros as it provides a way for users to use macros that may raise abort
conditions and still get useful error messages.

```move
module 0x42::macro_exporter {
public macro fun assert_false() {
assert!(false);
}
public macro fun abort_always() {
abort
}
public fun assert_false_fun() {
assert!(false); // Will always abort with the line number of this invocation
}
public fun abort_always_fun() {
abort // Will always abort with the line number of this invocation
}
}
module 0x42::user_module {
use 0x42::macro_exporter::{
assert_false,
abort_always,
assert_false_fun,
abort_always_fun
};
fun invoke_assert_false() {
assert_false!(); // Will abort with the line number of this invocation
}
fun invoke_abort_always() {
abort_always!(); // Will abort with the line number of this invocation
}
fun invoke_assert_false_fun() {
assert_false_fun(); // Will abort with the line number of the assertion in `assert_false_fun`
}
fun invoke_abort_always_fun() {
abort_always_fun(); // Will abort with the line number of the `abort` in `abort_always_fun`
}
}
```

## Inflating Clever Abort Codes

Precisely, the layout of a clever abort code is as follows:

```
|<tagbit>|<reserved>|<source line number>|<module identifier index>|<module constant index>|
+--------+----------+--------------------+-------------------------+-----------------------+
| 1-bit | 15-bits | 16-bits | 16-bits | 16-bits |
```

Note that the Move abort will come with some additional information -- importantly in our case the
module where the error occurred. This is important because the identifier index, and constant index
are relative to the module's identifier and constant tables (if not set the sentinel values).

> To decode a clever abort code, you will need to know the module where the error occurred if either
> the identifier index or constant index are not set to the sentinel value of `0xffff`.
In pseudo-code, you can decode a clever abort code as follows:

```rust
// Information available in the MoveAbort
let clever_abort_code: u64 = ...;
let (package_id, module_name): (PackageStorageId, ModuleName) = ...;

let is_clever_abort = (clever_abort_code & 0x8000_0000_0000_0000) != 0;

if is_clever_abort {
// Get line number, identifier index, and constant index
// Identifier and constant index are sentinel values if set to '0xffff'
let line_number = ((clever_abort_code & 0x0000_ffff_0000_0000) >> 32) as u16;
let identifier_index = ((clever_abort_code & 0x0000_0000_ffff_0000) >> 16) as u16;
let constant_index = ((clever_abort_code & 0x0000_0000_0000_ffff)) as u16;

// Print the line error message
print!("Error from '{}::{}' (line {})", package_id, module_name, line_number);

// No need to print anything or load the module if both are sentinel values
if identifier_index == 0xffff && constant_index == 0xffff {
return;
}

// Only needed if constant name and value are not 0xffff
let module: CompiledModule = fetch_module(package_id, module_name);

// Print the constant name (if any)
if identifier_index != 0xffff {
let constant_name = get_identifier_at_table_index(module, identifier_index);
print!(", '{}'", constant_name);
}

// Print the constant value (if any)
if constant_index != 0xffff {
let constant_value = get_constant_at_table_index(module, constant_index).deserialize_on_constant_type().to_string();
print!(": {}", constant_value);
}

return;
}
```

0 comments on commit 6f871c0

Please sign in to comment.