Repository: bytecodealliance/wasm-tools
Pull request #2128 (2025-04-08 04:30:21)

Improve text-format parsing of offset/align and update spec testsuite
The memory64/wasm-3.0 features update the type of memop `align=`_N_ and `offset=`_N_ to u64 in the text format. The `align.wast` test now includes an example with the largest possible values and also uses underscores in them (https://github.com/WebAssembly/spec/pull/1888). This updates the `wast` parsing logic to support this (by using the lexer), and updates the spec testsuite.


Patch:
diff --git a/crates/wast/src/core/expr.rs b/crates/wast/src/core/expr.rs
index 9fb25ffe..8f3c44cb 100644
--- a/crates/wast/src/core/expr.rs
+++ b/crates/wast/src/core/expr.rs
@@ -2,7 +2,8 @@ use crate::annotation;
 use crate::core::*;
 use crate::encode::Encode;
 use crate::kw;
-use crate::parser::{Cursor, Parse, Parser, Result};
+use crate::lexer::{Lexer, Token, TokenKind};
+use crate::parser::{Parse, Parser, Result};
 use crate::token::*;
 use std::mem;
 
@@ -1486,7 +1487,7 @@ pub struct MemArg<'a> {
     ///
     /// This is not stored as a log, this is the actual alignment (e.g. 1, 2, 4,
     /// 8, etc).
-    pub align: u32,
+    pub align: u64,
     /// The offset, in bytes of this access.
     pub offset: u64,
     /// The memory index we're accessing
@@ -1494,12 +1495,8 @@ pub struct MemArg<'a> {
 }
 
 impl<'a> MemArg<'a> {
-    fn parse(parser: Parser<'a>, default_align: u32) -> Result<Self> {
-        fn parse_field<T>(
-            name: &str,
-            parser: Parser<'_>,
-            f: impl FnOnce(Cursor<'_>, &str, u32) -> Result<T>,
-        ) -> Result<Option<T>> {
+    fn parse(parser: Parser<'a>, default_align: u64) -> Result<Self> {
+        fn parse_field(name: &str, parser: Parser<'_>) -> Result<Option<u64>> {
             parser.step(|c| {
                 let (kw, rest) = match c.keyword()? {
                     Some(p) => p,
@@ -1513,33 +1510,32 @@ impl<'a> MemArg<'a> {
                     return Ok((None, c));
                 }
                 let num = &kw[1..];
-                let num = if let Some(stripped) = num.strip_prefix("0x") {
-                    f(c, stripped, 16)?
-                } else {
-                    f(c, num, 10)?
-                };
-
-                Ok((Some(num), rest))
-            })
-        }
-
-        fn parse_u32(name: &str, parser: Parser<'_>) -> Result<Option<u32>> {
-            parse_field(name, parser, |c, num, radix| {
-                u32::from_str_radix(num, radix).map_err(|_| c.error("i32 constant out of range"))
-            })
-        }
-
-        fn parse_u64(name: &str, parser: Parser<'_>) -> Result<Option<u64>> {
-            parse_field(name, parser, |c, num, radix| {
-                u64::from_str_radix(num, radix).map_err(|_| c.error("i64 constant out of range"))
+                let lexer = Lexer::new(num);
+                let mut pos = 0;
+                if let Ok(Some(
+                    token @ Token {
+                        kind: TokenKind::Integer(integer_kind),
+                        ..
+                    },
+                )) = lexer.parse(&mut pos)
+                {
+                    let int = token.integer(lexer.input(), integer_kind);
+                    let (s, base) = int.val();
+                    let value = u64::from_str_radix(s, base);
+                    return match value {
+                        Ok(n) => Ok((Some(n), rest)),
+                        Err(_) => Err(c.error("u64 constant out of range")),
+                    };
+                }
+                Err(c.error("expected u64 integer constant"))
             })
         }
 
         let memory = parser
             .parse::<Option<_>>()?
             .unwrap_or_else(|| Index::Num(0, parser.prev_span()));
-        let offset = parse_u64("offset", parser)?.unwrap_or(0);
-        let align = match parse_u32("align", parser)? {
+        let offset = parse_field("offset", parser)?.unwrap_or(0);
+        let align = match parse_field("align", parser)? {
             Some(n) if !n.is_power_of_two() => {
                 return Err(parser.error("alignment must be a power of two"))
             }
@@ -1564,7 +1560,7 @@ pub struct LoadOrStoreLane<'a> {
 }
 
 impl<'a> LoadOrStoreLane<'a> {
-    fn parse(parser: Parser<'a>, default_align: u32) -> Result<Self> {
+    fn parse(parser: Parser<'a>, default_align: u64) -> Result<Self> {
         // This is sort of funky. The first integer we see could be the lane
         // index, but it could also be the memory index. To determine what it is
         // then if we see a second integer we need to look further.


Test patch:
diff --git a/tests/snapshots/testsuite/proposals/custom-page-sizes/memory_max.wast.json b/tests/snapshots/testsuite/proposals/custom-page-sizes/memory_max.wast.json
index 6a2208e2..db6be36a 100644
--- a/tests/snapshots/testsuite/proposals/custom-page-sizes/memory_max.wast.json
+++ b/tests/snapshots/testsuite/proposals/custom-page-sizes/memory_max.wast.json
@@ -16,15 +16,15 @@
       "text": "unknown import"
     },
     {
-      "type": "assert_malformed",
-      "line": 32,
-      "filename": "memory_max.2.wat",
-      "module_type": "text",
-      "text": "constant out of range"
+      "type": "assert_invalid",
+      "line": 29,
+      "filename": "memory_max.2.wasm",
+      "module_type": "binary",
+      "text": "memory size must be at most"
     },
     {
       "type": "assert_invalid",
-      "line": 37,
+      "line": 35,
       "filename": "memory_max.3.wasm",
       "module_type": "binary",
       "text": "memory size must be at most"
diff --git a/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast.json b/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast.json
index 4b1d3a30..25b88db0 100644
--- a/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast.json
+++ b/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast.json
@@ -1873,6 +1873,33 @@
       "filename": "align.112.wasm",
       "module_type": "binary",
       "text": "type mismatch"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 968,
+      "filename": "align.113.wasm",
+      "module_type": "binary",
+      "text": "malformed memop flags"
+    },
+    {
+      "type": "assert_malformed",
+      "line": 987,
+      "filename": "align.114.wasm",
+      "module_type": "binary",
+      "text": "malformed memop flags"
+    },
+    {
+      "type": "module",
+      "line": 1005,
+      "filename": "align.115.wasm",
+      "module_type": "binary"
+    },
+    {
+      "type": "assert_invalid",
+      "line": 1015,
+      "filename": "align.116.wasm",
+      "module_type": "binary",
+      "text": "alignment must not be larger than natural"
     }
   ]
 }
\ No newline at end of file
diff --git a/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast/163.print b/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast/163.print
new file mode 100644
index 00000000..9befd736
--- /dev/null
+++ b/tests/snapshots/testsuite/proposals/wasm-3.0/align.wast/163.print
@@ -0,0 +1,9 @@
+(module
+  (type (;0;) (func))
+  (memory (;0;) i64 1)
+  (func (;0;) (type 0)
+    i64.const 0
+    i32.load offset=18446744073709551615
+    drop
+  )
+)
diff --git a/tests/testsuite b/tests/testsuite
index d76759e7..1db33e84 160000
--- a/tests/testsuite
+++ b/tests/testsuite
@@ -1,1 +1,1 @@
-Subproject commit d76759e746f3564a03f6106ae19679742f2a1831
+Subproject commit 1db33e84b263e051c4d9661f0593975382f0a4f1