Skip to content

Attribute for checking of trivial encoding and decoding#7575

Draft
xunilrj wants to merge 12 commits intomasterfrom
xunilrj/trivial-checks
Draft

Attribute for checking of trivial encoding and decoding#7575
xunilrj wants to merge 12 commits intomasterfrom
xunilrj/trivial-checks

Conversation

@xunilrj
Copy link
Contributor

@xunilrj xunilrj commented Mar 17, 2026

Description

Closes #7567.

Checklist

  • I have linked to any relevant issues.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have updated the documentation where relevant (API docs, the reference, and the Sway book).
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added (or requested a maintainer to add) the necessary Breaking* or New Feature labels where relevant.
  • I have done my best to ensure that my PR adheres to the Fuel Labs Code Review Standards.
  • I have requested a review from the relevant team or maintainers.

@cursor
Copy link

cursor bot commented Mar 17, 2026

PR Summary

High Risk
High risk because it changes compiler attribute parsing and IR/codegen paths for type layout/triviality, adds a new intrinsic, and introduces new stdlib ABI encode/decode behavior that can affect contract/script interfaces.

Overview
Adds a new #[require(...)] attribute (with trivially_encodable/trivially_decodable args) and wires it through attribute validation, metadata collection, and IR generation so annotated structs can fail compilation early with a new TrivialCheckFailed diagnostic that pinpoints non-trivially-decodable fields and suggests fixes.

Extends type-layout/triviality machinery by enriching MemoryRepresentation::Blob with optional valid-value ranges (notably for bool and enum discriminants) and introduces the __enum_discriminant_count::<T>() intrinsic to support safe enum validation.

Updates the stdlib codec with TrivialBool and TrivialEnum<T> wrappers (plus tests) to allow “trivial” ABI decode while still enforcing validity at unwrap-time, and adds reference docs for trivial encoding/decoding (with a small SUMMARY entry formatting issue).

Written by Cursor Bugbot for commit e630091. This will update automatically on new commits. Configure here.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Debug statement accidentally committed to production
    • Removed the stray dbg!(&t.decls_to_check) call from compile_to_asm so compilation no longer emits unintended stderr output.
  • ✅ Fixed: Misplaced test: non-trivially-decodable struct in should_pass
    • Updated the attribute_require should-pass program to use trivially decodable field types (u64 and nested u64 struct) so it now matches its expected passing placement.
  • ✅ Fixed: Attribute value required but silently ignored in check
    • The require check now reads arg.value and only enforces trivially_decodable when the value is explicitly true ("true" or true).
  • ✅ Fixed: Require attribute check skipped for predicates and contracts
    • Threaded decls_to_check through predicate and contract compilation paths so #[require(trivially_decodable = "true")] is validated there too.

Create PR

Or push these changes by commenting:

@cursor push 73287ee042
Preview (73287ee042)
diff --git a/sway-core/src/ir_generation.rs b/sway-core/src/ir_generation.rs
--- a/sway-core/src/ir_generation.rs
+++ b/sway-core/src/ir_generation.rs
@@ -399,6 +399,7 @@
             &mut panicking_fn_cache,
             &test_fns,
             &mut compiled_fn_cache,
+            decls_to_check,
         ),
         ty::TyProgramKind::Contract {
             entry_function,
@@ -409,6 +410,7 @@
             abi_entries,
             namespace,
             declarations,
+            decls_to_check,
             &logged_types,
             &messages_types,
             panic_occurrences,

diff --git a/sway-core/src/ir_generation/compile.rs b/sway-core/src/ir_generation/compile.rs
--- a/sway-core/src/ir_generation/compile.rs
+++ b/sway-core/src/ir_generation/compile.rs
@@ -1,9 +1,19 @@
 use crate::{
-    Engines, PanicOccurrences, PanickingCallOccurrences, TypeInfo, decl_engine::{DeclEngineGet, DeclId, DeclRefFunction}, ir_generation::{
-        KeyedTyFunctionDecl, PanickingFunctionCache, convert::convert_resolved_typeid_no_span
-    }, language::{
-        Visibility, ty::{self, StructDecl, TyDecl}
-    }, metadata::MetadataManager, namespace::ResolvedDeclaration, semantic_analysis::namespace, transform::AttributeKind, type_system::TypeId, types::{LogId, MessageId}
+    decl_engine::{DeclEngineGet, DeclId, DeclRefFunction},
+    ir_generation::{
+        convert::convert_resolved_typeid_no_span, KeyedTyFunctionDecl, PanickingFunctionCache,
+    },
+    language::{
+        ty::{self, StructDecl, TyDecl},
+        Visibility,
+    },
+    metadata::MetadataManager,
+    namespace::ResolvedDeclaration,
+    semantic_analysis::namespace,
+    transform::AttributeKind,
+    type_system::TypeId,
+    types::{LogId, MessageId},
+    Engines, PanicOccurrences, PanickingCallOccurrences, TypeInfo,
 };
 
 use super::{
@@ -13,7 +23,7 @@
     CompiledFunctionCache,
 };
 
-use sway_ast::attribute::REQUIRE_ARG_NAME_TRIVIALLY_DECODABLE;
+use sway_ast::{attribute::REQUIRE_ARG_NAME_TRIVIALLY_DECODABLE, Literal};
 use sway_error::{error::CompileError, handler::Handler};
 use sway_ir::{metadata::combine as md_combine, *};
 use sway_types::{Ident, Span, Spanned};
@@ -104,6 +114,7 @@
     panicking_fn_cache: &mut PanickingFunctionCache,
     test_fns: &[(Arc<ty::TyFunctionDecl>, DeclRefFunction)],
     compiled_fn_cache: &mut CompiledFunctionCache,
+    decls_to_check: &[TyDecl],
 ) -> Result<Module, Vec<CompileError>> {
     let module = Module::new(context, Kind::Predicate);
 
@@ -138,7 +149,7 @@
         panicking_fn_cache,
         None,
         compiled_fn_cache,
-        &[],
+        decls_to_check,
     )?;
     compile_tests(
         engines,
@@ -164,6 +175,7 @@
     abi_entries: &[DeclId<ty::TyFunctionDecl>],
     namespace: &namespace::Namespace,
     declarations: &[ty::TyDecl],
+    decls_to_check: &[TyDecl],
     logged_types_map: &HashMap<TypeId, LogId>,
     messages_types_map: &HashMap<TypeId, MessageId>,
     panic_occurrences: &mut PanicOccurrences,
@@ -217,7 +229,7 @@
             panicking_fn_cache,
             None,
             compiled_fn_cache,
-            &[],
+            decls_to_check,
         )?;
     } else {
         // In the case of the encoding v0, we need to compile individual ABI entries
@@ -602,7 +614,18 @@
                 for (_, atts) in atts {
                     for att in atts.iter() {
                         for arg in att.args.iter() {
-                            if arg.name.as_str() == REQUIRE_ARG_NAME_TRIVIALLY_DECODABLE && !is_type_trivially_decodable(*decl_id) {
+                            let requires_trivially_decodable = matches!(
+                                &arg.value,
+                                Some(Literal::String(val)) if val.parsed == "true"
+                            ) || matches!(
+                                &arg.value,
+                                Some(Literal::Bool(val)) if val.kind.into()
+                            );
+
+                            if arg.name.as_str() == REQUIRE_ARG_NAME_TRIVIALLY_DECODABLE
+                                && requires_trivially_decodable
+                                && !is_type_trivially_decodable(*decl_id)
+                            {
                                 let mut infos = vec![];
                                 let mut helps = vec![];
                                 let mut bottom_helps = BTreeSet::new();

diff --git a/sway-core/src/lib.rs b/sway-core/src/lib.rs
--- a/sway-core/src/lib.rs
+++ b/sway-core/src/lib.rs
@@ -1188,10 +1188,6 @@
         experimental,
     )?;
 
-    if let Ok(t) = ast_res.typed.as_ref() {
-        dbg!(&t.decls_to_check);
-    }
-
     ast_to_asm(handler, engines, &ast_res, build_config, experimental)
 }
 

diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/another_file.sw b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/another_file.sw
--- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/another_file.sw
+++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/another_file.sw
@@ -1,5 +1,5 @@
 library;
 
 pub struct InnerStruct {
-    pub a: bool,
-}
\ No newline at end of file
+    pub a: u64,
+}

diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/main.sw
--- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/main.sw
+++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes/attribute_require/src/main.sw
@@ -5,21 +5,15 @@
 
 #[require(trivially_decodable = "true")]
 struct MyStruct {
-    a: bool,
+    a: u64,
     b: InnerStruct,
-    c: SomeEnum,
-    d: Vec<u64>,
+    c: u64,
+    d: u64,
 }
 
-enum SomeEnum {
-    A: ()
-}
-
 fn main(s: MyStruct) {
     __log(s.a);
     __log(s.b.a);
     __log(s.c);
     __log(s.d);
-    let a = SomeEnum::A;
-    __log(a);
 }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.


if let Ok(t) = ast_res.typed.as_ref() {
dbg!(&t.decls_to_check);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug statement accidentally committed to production

Medium Severity

A dbg! macro call printing decls_to_check was left in the production code path of compile_to_asm. This runs on every compilation, printing debug output to stderr for all users of the compiler.

Fix in Cursor Fix in Web

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 17, 2026

Merging this PR will not alter performance

✅ 25 untouched benchmarks


Comparing xunilrj/trivial-checks (e630091) with master (cf207d2)

Open in CodSpeed

panicking_fn_cache,
None,
compiled_fn_cache,
&[],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial check skipped for contracts and predicates

Medium Severity

decls_to_check is extracted from the program but only passed through to compile_entry_function for scripts. For contracts and predicates, &[] is hardcoded, so #[require] attribute checks silently do nothing. The documentation specifically mentions contracts and predicates as supported targets.

Additional Locations (2)
Fix in Cursor Fix in Web

vec![TypeMetadata::CheckDecl(TyDecl::StructDecl(decl.clone()))]
} else {
vec![]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly broad attribute check collects all attributed structs

Low Severity

The check !d.attributes.is_empty() adds every struct with any attribute (including doc comments, #[cfg], #[deprecated], etc.) to decls_to_check. This is overly broad — only structs with AttributeKind::Require need to be collected, since that's all compile_entry_function processes.

Fix in Cursor Fix in Web

TypeContent::Bool => MemoryRepresentation::Blob {
len_in_bytes: 1,
range: Some(0..2),
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bool runtime representation uses range, breaking triviality equality

Medium Severity

The range field on MemoryRepresentation::Blob is included in the derived PartialEq. The runtime representation for bool uses range: Some(0..2) while the encoding representation uses range: None. This means bool is correctly detected as non-trivially-decodable. However, u8 has range: None in both, so u8 is treated as trivially decodable — yet the documentation table says u8 is trivially decodable, so that's fine. The real concern is enum discriminants: encoding uses range: Some(0..N) but runtime uses range: None (via the Or variant's inner Blobs). This means the range field creates asymmetry that controls triviality purely through equality — an implicit and fragile mechanism that's easy to get wrong as the code evolves.

Additional Locations (1)
Fix in Cursor Fix in Web

fn trivial_bool_when_invalid_is_valid() {
let slice = encode(TrivialBool { value: 2 });
abi_decode::<TrivialBool>(slice).unwrap();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate test doesn't verify is_valid method

Medium Severity

trivial_bool_when_invalid_is_valid and trivial_bool_when_invalid_unwrap have identical bodies — both call .unwrap(). The first test's name implies it verifies that is_valid() returns false for an invalid value, but it never calls is_valid(). This means is_valid() on TrivialBool has zero test coverage for the invalid case.

Additional Locations (1)
Fix in Cursor Fix in Web

span: span.clone(),
};
Ok((intrinsic_function, ctx.engines.te().id_of_u64()))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No enum type validation for EnumDiscriminantCount intrinsic

Medium Severity

type_check_enum_discriminant_count validates argument count and type argument count but never verifies the type argument is actually an enum. Since TrivialEnum<T> has no generic constraint requiring T to be an enum, a user could write TrivialEnum<u64> which would pass type checking but hit todo!("ICE") in IR generation, crashing the compiler.

Additional Locations (1)
Fix in Cursor Fix in Web


// Require Attributes
pub const REQUIRE_ATTRIBUTE_NAME: &str = "require";
pub const REQUIRE_ARG_NAME_TRIVIALLY_ENCODABLE: &str = "trivially_encodable";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivially encodable check defined but never enforced

Medium Severity

REQUIRE_ARG_NAME_TRIVIALLY_ENCODABLE is defined and registered as a valid argument for the #[require] attribute, but the checking logic in compile_entry_function only checks for REQUIRE_ARG_NAME_TRIVIALLY_DECODABLE. A user writing #[require(trivially_encodable = "true")] gets no compiler error and no validation — the attribute is silently accepted and ignored, giving a false sense of safety.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 10 total unresolved issues (including 7 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encodable require argument is ignored

Medium Severity

run_ir_decl_checks only handles trivially_decodable and never processes trivially_encodable, even though the attribute parser accepts both keys. Using #[require(trivially_encodable = ...)] currently performs no validation.

Fix in Cursor Fix in Web

} else {
vec![]
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Require allowed on enums but never checked

Medium Severity

#[require] is accepted on enums, but enum declarations are excluded from TypeMetadata::CheckDecl collection, so enum annotations never reach the checker.

Additional Locations (1)
Fix in Cursor Fix in Web

}
Intrinsic::EnumDiscriminantCount => {
todo!();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Const-eval intrinsic branch still unimplemented

High Severity

Intrinsic::EnumDiscriminantCount in const-eval calls todo!(). Using this intrinsic in any constant-evaluated context will panic the compiler.

Fix in Cursor Fix in Web

@ironcev ironcev changed the title Attribute for checking trivially of encoding and decoding Attribute for checking of trivially encoding and decoding Mar 26, 2026
@ironcev ironcev changed the title Attribute for checking of trivially encoding and decoding Attribute for checking of trivial encoding and decoding Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler attribute to guarantee that types are trivially encodable/decodable

1 participant