Repository: 0xMiden/miden-vm (Rust)

Issue:
allow odd number of characters in hex constants
the following code:

```
begin
    push.0x100
end
```

fails to assemble with the following error code:

```
Failed to parse program file `t.masm` - parsing error at [2:5]: malformed instruction 'push.0x100', parameter 0x100 is invalid: hex string '100' does not contain an even number of characters
```

However, `0x100` is a valid representation of the number `128`. As an example, the following program:

```rust
fn main() {
    println!("{}", format!("0x{:02x}", 256));
}
```

Will print `0x100` when executed. This is causing test failures in `miden-base` when similar logic is used to format an account id for testing.


Discussion:
Is this fixed? If not, I can pick this up.
I believe the parser still requires there to be an even number of hex digits, so I believe this is still an open issue @Bisht13 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b451730e8..c5b5b71c04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 #### Changes
 
 - [BREAKING] Incremented MSRV to 1.90.
+- Fix ability to parse odd-length hex strings ([#2196](https://github.com/0xMiden/miden-vm/pull/2196)).
 
 ## 0.18.0 (2025-09-21)
 
diff --git a/crates/assembly-syntax/src/parser/lexer.rs b/crates/assembly-syntax/src/parser/lexer.rs
index 48d26f29af..a3b4c636ce 100644
--- a/crates/assembly-syntax/src/parser/lexer.rs
+++ b/crates/assembly-syntax/src/parser/lexer.rs
@@ -1,4 +1,4 @@
-use alloc::string::String;
+use alloc::{borrow::Cow, string::String};
 use core::{num::IntErrorKind, ops::Range};
 
 use miden_debug_types::{ByteOffset, SourceId, SourceSpan};
@@ -556,10 +556,10 @@ impl<'input> Lexer<'input> {
             // it is invalid
             let c2 = self.read();
             if !c2.is_ascii_hexdigit() {
-                return Err(ParsingError::InvalidHexLiteral {
-                    span: self.span(),
-                    kind: HexErrorKind::Invalid,
-                });
+                // For odd-length hex strings, we need to handle this by
+                // adjusting the span to not include the unpaired character
+                // and let parse_hex handle the padding
+                break;
             }
             self.skip();
         }
@@ -610,15 +610,31 @@ impl<'input> Iterator for Lexer<'input> {
 // HELPER FUNCTIONS
 // ================================================================================================
 
+fn pad_hex_if_needed<'a>(hex: &'a str) -> Cow<'a, str> {
+    if hex.len().is_multiple_of(2) {
+        Cow::Borrowed(hex)
+    } else {
+        // allocate once, with exact capacity
+        let mut s = alloc::string::String::with_capacity(hex.len() + 1);
+        s.push('0');
+        s.push_str(hex);
+        Cow::Owned(s)
+    }
+}
+
 fn parse_hex<'input>(
     span: SourceSpan,
     hex_digits: &'input str,
 ) -> Result<Token<'input>, ParsingError> {
     use miden_core::{FieldElement, StarkField};
+
+    // Handle odd-length hex strings by padding with a leading zero
+    let hex_digits = pad_hex_if_needed(hex_digits);
+
     match hex_digits.len() {
-        // Felt
+        // Felt - allow all even lengths up to 16
         n if n <= 16 && n.is_multiple_of(2) => {
-            let value = u64::from_str_radix(hex_digits, 16).map_err(|error| {
+            let value = u64::from_str_radix(&hex_digits, 16).map_err(|error| {
                 ParsingError::InvalidLiteral {
                     span,
                     kind: int_error_kind_to_literal_error_kind(
@@ -667,9 +683,6 @@ fn parse_hex<'input>(
         },
         // Invalid
         n if n > 64 => Err(ParsingError::InvalidHexLiteral { span, kind: HexErrorKind::TooLong }),
-        n if !n.is_multiple_of(2) && n < 64 => {
-            Err(ParsingError::InvalidHexLiteral { span, kind: HexErrorKind::MissingDigits })
-        },
         _ => Err(ParsingError::InvalidHexLiteral { span, kind: HexErrorKind::Invalid }),
     }
 }


Test patch:
diff --git a/miden-vm/tests/integration/operations/io_ops/constant_ops.rs b/miden-vm/tests/integration/operations/io_ops/constant_ops.rs
index 04092a8580..248559f5c2 100644
--- a/miden-vm/tests/integration/operations/io_ops/constant_ops.rs
+++ b/miden-vm/tests/integration/operations/io_ops/constant_ops.rs
@@ -1,3 +1,5 @@
+use miden_utils_testing::proptest::prelude::*;
+
 use super::build_op_test;
 
 // PUSHING VALUES ONTO THE STACK (PUSH)
@@ -76,3 +78,44 @@ fn push_without_separator() {
     let test = build_op_test!(asm_op);
     test.expect_stack(&expected);
 }
+
+#[test]
+fn push_odd_hex_length_original_issue() {
+    // Test for issue 1302: now fixed with padding
+    let asm_op = "push.0x100";
+
+    let test = build_op_test!(asm_op);
+    test.expect_stack(&[256]); // 0x100 = 256 (now works with padding)
+}
+
+proptest! {
+    #[test]
+    fn proptest_push_all_hex_lengths(
+        // Generate random hex strings of all lengths between 1 and 16
+        length in 1usize..=16,
+        hex_bytes in prop::collection::vec(prop::num::u8::ANY, 1..=16)
+    ) {
+        // Convert to hex string, taking only the lower 4 bits of each byte
+        let hex_str: String = hex_bytes.iter()
+            .take(length)
+            .map(|&b| format!("{:x}", b & 0xF))
+            .collect();
+
+        // Calculate expected value by padding with leading zero for odd lengths
+        let padded_hex = if hex_str.len() % 2 == 1 {
+            format!("0{}", hex_str)
+        } else {
+            hex_str.clone()
+        };
+
+        // Parse as 64-bit integer (this should always succeed for valid hex)
+        let expected = u64::from_str_radix(&padded_hex, 16).unwrap();
+
+        // Build assembly operation
+        let asm_op = format!("push.0x{}", hex_str);
+
+        // Test that it parses and executes correctly
+        let test = build_op_test!(&asm_op);
+        test.expect_stack(&[expected]);
+    }
+}