Skip to content

Commit 26d426e

Browse files
authored
fix: map IntValue to unsigned repr when serializing (#2413)
to match Rust definition Closes #2409
1 parent 939912a commit 26d426e

File tree

3 files changed

+66
-4
lines changed

3 files changed

+66
-4
lines changed

hugr-py/src/hugr/std/int.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,39 @@ def _int_tv(index: int) -> tys.ExtType:
5252
INT_T = int_t(5)
5353

5454

55+
def _to_unsigned(val: int, bits: int) -> int:
56+
"""Convert a signed integer to its unsigned representation
57+
in twos-complement form.
58+
59+
Positive integers are unchanged, while negative integers
60+
are converted by adding 2^bits to the value.
61+
62+
Raises ValueError if the value is out of range for the given bit width
63+
(valid range is [-2^(bits-1), 2^(bits-1)-1]).
64+
"""
65+
half_max = 1 << (bits - 1)
66+
min_val = -half_max
67+
max_val = half_max - 1
68+
if val < min_val or val > max_val:
69+
msg = f"Value {val} out of range for {bits}-bit signed integer."
70+
raise ValueError(msg) #
71+
72+
if val < 0:
73+
return (1 << bits) + val
74+
return val
75+
76+
5577
@dataclass
5678
class IntVal(val.ExtensionValue):
57-
"""Custom value for an integer."""
79+
"""Custom value for a signed integer."""
5880

5981
v: int
6082
width: int = field(default=5)
6183

6284
def to_value(self) -> val.Extension:
6385
name = "ConstInt"
64-
payload = {"log_width": self.width, "value": self.v}
86+
unsigned = _to_unsigned(self.v, 1 << self.width)
87+
payload = {"log_width": self.width, "value": unsigned}
6588
return val.Extension(
6689
name,
6790
typ=int_t(self.width),
@@ -72,8 +95,9 @@ def __str__(self) -> str:
7295
return f"{self.v}"
7396

7497
def to_model(self) -> model.Term:
98+
unsigned = _to_unsigned(self.v, 1 << self.width)
7599
return model.Apply(
76-
"arithmetic.int.const", [model.Literal(self.width), model.Literal(self.v)]
100+
"arithmetic.int.const", [model.Literal(self.width), model.Literal(unsigned)]
77101
)
78102

79103

hugr-py/tests/test_prelude.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import pytest
2+
13
from hugr.build.dfg import Dfg
4+
from hugr.std.int import IntVal, int_t
25
from hugr.std.prelude import STRING_T, StringVal
36

47
from .conftest import validate
@@ -16,3 +19,38 @@ def test_string_val():
1619
dfg.set_outputs(v)
1720

1821
validate(dfg.hugr)
22+
23+
24+
@pytest.mark.parametrize(
25+
("log_width", "v", "unsigned"),
26+
[
27+
(5, 1, 1),
28+
(4, 0, 0),
29+
(6, 42, 42),
30+
(2, -1, 15),
31+
(1, -2, 2),
32+
(3, -23, 233),
33+
(3, -256, None),
34+
(2, 16, None),
35+
],
36+
)
37+
def test_int_val(log_width: int, v: int, unsigned: int | None):
38+
val = IntVal(v, log_width)
39+
if unsigned is None:
40+
with pytest.raises(
41+
ValueError,
42+
match=f"Value {v} out of range for {1<<log_width}-bit signed integer.",
43+
):
44+
val.to_value()
45+
return
46+
ext_val = val.to_value()
47+
48+
assert ext_val.name == "ConstInt"
49+
assert ext_val.typ == int_t(log_width)
50+
assert ext_val.val == {"log_width": log_width, "value": unsigned}
51+
52+
dfg = Dfg()
53+
o = dfg.load(val)
54+
dfg.set_outputs(o)
55+
56+
validate(dfg.hugr)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)