From 9745df7559a933a8dfef2c52a7e209ba28a13ac8 Mon Sep 17 00:00:00 2001 From: Seyon Sivarajah Date: Thu, 3 Jul 2025 23:26:28 +0100 Subject: [PATCH] fix: map IntValue to unsigned repr when serializing to match Rust definition Closes #2409 --- hugr-py/src/hugr/std/int.py | 30 ++++++++++++++++++++++++--- hugr-py/tests/test_prelude.py | 38 +++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/hugr-py/src/hugr/std/int.py b/hugr-py/src/hugr/std/int.py index f58bc9e3eb..4107fcd284 100644 --- a/hugr-py/src/hugr/std/int.py +++ b/hugr-py/src/hugr/std/int.py @@ -52,16 +52,39 @@ def _int_tv(index: int) -> tys.ExtType: INT_T = int_t(5) +def _to_unsigned(val: int, bits: int) -> int: + """Convert a signed integer to its unsigned representation + in twos-complement form. + + Positive integers are unchanged, while negative integers + are converted by adding 2^bits to the value. + + Raises ValueError if the value is out of range for the given bit width + (valid range is [-2^(bits-1), 2^(bits-1)-1]). + """ + half_max = 1 << (bits - 1) + min_val = -half_max + max_val = half_max - 1 + if val < min_val or val > max_val: + msg = f"Value {val} out of range for {bits}-bit signed integer." + raise ValueError(msg) # + + if val < 0: + return (1 << bits) + val + return val + + @dataclass class IntVal(val.ExtensionValue): - """Custom value for an integer.""" + """Custom value for a signed integer.""" v: int width: int = field(default=5) def to_value(self) -> val.Extension: name = "ConstInt" - payload = {"log_width": self.width, "value": self.v} + unsigned = _to_unsigned(self.v, 1 << self.width) + payload = {"log_width": self.width, "value": unsigned} return val.Extension( name, typ=int_t(self.width), @@ -72,8 +95,9 @@ def __str__(self) -> str: return f"{self.v}" def to_model(self) -> model.Term: + unsigned = _to_unsigned(self.v, 1 << self.width) return model.Apply( - "arithmetic.int.const", [model.Literal(self.width), model.Literal(self.v)] + "arithmetic.int.const", [model.Literal(self.width), model.Literal(unsigned)] ) diff --git a/hugr-py/tests/test_prelude.py b/hugr-py/tests/test_prelude.py index c2ef2cbeec..71af3d4e06 100644 --- a/hugr-py/tests/test_prelude.py +++ b/hugr-py/tests/test_prelude.py @@ -1,4 +1,7 @@ +import pytest + from hugr.build.dfg import Dfg +from hugr.std.int import IntVal, int_t from hugr.std.prelude import STRING_T, StringVal from .conftest import validate @@ -16,3 +19,38 @@ def test_string_val(): dfg.set_outputs(v) validate(dfg.hugr) + + +@pytest.mark.parametrize( + ("log_width", "v", "unsigned"), + [ + (5, 1, 1), + (4, 0, 0), + (6, 42, 42), + (2, -1, 15), + (1, -2, 2), + (3, -23, 233), + (3, -256, None), + (2, 16, None), + ], +) +def test_int_val(log_width: int, v: int, unsigned: int | None): + val = IntVal(v, log_width) + if unsigned is None: + with pytest.raises( + ValueError, + match=f"Value {v} out of range for {1<