Skip to content

Commit d50cfec

Browse files
committed
interpreter: Add hex/octal/binary support to str and int primitives
Add support for parsing and formatting integers in hexadecimal, octal, and binary representations to complement the existing decimal support. str.to_int() now accepts strings with 0x/0o/0b prefixes and optional leading signs (+/-), enabling direct parsing of hex, octal, and binary literals while maintaining full backward compatibility with decimal strings including those with leading zeros. int.to_string() gains a new format keyword argument that accepts 'dec', 'hex', 'oct', or 'bin', allowing integers to be formatted with appropriate prefixes (0x, 0o, 0b). The format parameter works correctly with the existing fill parameter and handles negative numbers properly. Round-trip conversions are fully supported: values formatted with int.to_string(format: 'hex') can be parsed back with str.to_int(). Fixes: #2047 Fixes: #15201
1 parent f5d81d0 commit d50cfec

File tree

5 files changed

+184
-4
lines changed

5 files changed

+184
-4
lines changed

docs/markdown/Syntax.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,31 @@ string_var = '42'
8080
num = string_var.to_int()
8181
```
8282

83+
Hexadecimal, octal, and binary strings can be converted to numbers since
84+
1.10.0:
85+
86+
```meson
87+
hex_var = '0xFF'.to_int() # 255
88+
oct_var = '0o755'.to_int() # 493
89+
bin_var = '0b1010'.to_int() # 10
90+
```
91+
8392
Numbers can be converted to a string:
8493

8594
```meson
8695
int_var = 42
8796
string_var = int_var.to_string()
8897
```
8998

99+
Numbers can be formatted as hexadecimal, octal, or binary strings since 1.10.0:
100+
101+
```meson
102+
int_var = 255
103+
hex_str = int_var.to_string(format: 'hex') # '0xff'
104+
oct_str = int_var.to_string(format: 'oct') # '0o377'
105+
bin_str = int_var.to_string(format: 'bin') # '0b11111111'
106+
```
107+
90108
## Booleans
91109

92110
A boolean is either `true` or `false`.

mesonbuild/interpreter/primitives/integer.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@
77
FeatureBroken, InvalidArguments, KwargInfo,
88
noKwargs, noPosargs, typed_operator, typed_kwargs
99
)
10+
from ..type_checking import in_set_validator
1011

1112
import typing as T
1213

1314
if T.TYPE_CHECKING:
15+
from typing_extensions import Literal, TypedDict
16+
1417
from ...interpreterbase import TYPE_var, TYPE_kwargs
1518

19+
class ToStringKw(TypedDict):
20+
21+
fill: int
22+
format: Literal["dec", "hex", "oct", "bin"]
23+
1624
class IntegerHolder(ObjectHolder[int]):
1725
# Operators that only require type checks
1826
TRIVIAL_OPERATORS = {
@@ -55,12 +63,20 @@ def is_odd_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:
5563

5664
@typed_kwargs(
5765
'to_string',
58-
KwargInfo('fill', int, default=0, since='1.3.0')
66+
KwargInfo('fill', int, default=0, since='1.3.0'),
67+
KwargInfo(
68+
"format",
69+
str,
70+
default="dec",
71+
since="1.10.0",
72+
validator=in_set_validator({"dec", "hex", "oct", "bin"}),
73+
),
5974
)
6075
@noPosargs
6176
@InterpreterObject.method('to_string')
62-
def to_string_method(self, args: T.List[TYPE_var], kwargs: T.Dict[str, T.Any]) -> str:
63-
return str(self.held_object).zfill(kwargs['fill'])
77+
def to_string_method(self, args: T.List[TYPE_var], kwargs: 'ToStringKw') -> str:
78+
format_codes = {"hex": "x", "oct": "o", "bin": "b", "dec": "d"}
79+
return format(self.held_object, f'#0{kwargs["fill"]}{format_codes[kwargs["format"]]}')
6480

6581
@typed_operator(MesonOperator.DIV, int)
6682
@InterpreterObject.operator(MesonOperator.DIV)

mesonbuild/interpreter/primitives/string.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,19 @@ def substring_method(self, args: T.Tuple[T.Optional[int], T.Optional[int]], kwar
132132
@InterpreterObject.method('to_int')
133133
def to_int_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
134134
try:
135-
return int(self.held_object)
135+
s = self.held_object.strip()
136+
s_unsigned = s.lstrip('+-').lower()
137+
138+
if s_unsigned.startswith('0x'):
139+
base = 16
140+
elif s_unsigned.startswith('0o'):
141+
base = 8
142+
elif s_unsigned.startswith('0b'):
143+
base = 2
144+
else:
145+
base = 10
146+
147+
return int(s, base)
136148
except ValueError:
137149
raise InvalidArguments(f'String {self.held_object!r} cannot be converted to int')
138150

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
project('test number base conversions')
2+
3+
### .to_int()
4+
5+
# hexadecimal strings
6+
assert('0x10'.to_int() == 16, 'Hex string conversion failed')
7+
assert('0X10'.to_int() == 16, 'Hex string conversion (uppercase X) failed')
8+
assert('0xff'.to_int() == 255, 'Hex string conversion (lowercase) failed')
9+
assert('0xFF'.to_int() == 255, 'Hex string conversion (uppercase) failed')
10+
assert('0xDEADBEEF'.to_int() == 3735928559, 'Large hex conversion failed')
11+
assert('0x1'.to_int() == 1, 'Small hex conversion failed')
12+
assert('0x0'.to_int() == 0, 'Zero hex conversion failed')
13+
14+
# signed hexadecimal strings
15+
assert('-0xf'.to_int() == -15, 'Negative hex string conversion failed')
16+
assert('+0x10'.to_int() == 16, 'Positive hex string conversion failed')
17+
assert('-0xFF'.to_int() == -255, 'Negative hex string (uppercase) conversion failed')
18+
19+
# octal strings
20+
assert('0o10'.to_int() == 8, 'Octal string conversion failed')
21+
assert('0O10'.to_int() == 8, 'Octal string conversion (uppercase O) failed')
22+
assert('0o77'.to_int() == 63, 'Octal string conversion failed')
23+
assert('0o755'.to_int() == 493, 'Octal permission-like conversion failed')
24+
assert('0o0'.to_int() == 0, 'Zero octal conversion failed')
25+
26+
# signed octal strings
27+
assert('-0o10'.to_int() == -8, 'Negative octal string conversion failed')
28+
assert('+0o77'.to_int() == 63, 'Positive octal string conversion failed')
29+
30+
# binary strings
31+
assert('0b10'.to_int() == 2, 'Binary string conversion failed')
32+
assert('0B10'.to_int() == 2, 'Binary string conversion (uppercase B) failed')
33+
assert('0b1111'.to_int() == 15, 'Binary string conversion failed')
34+
assert('0b11111111'.to_int() == 255, 'Binary byte conversion failed')
35+
assert('0b0'.to_int() == 0, 'Zero binary conversion failed')
36+
37+
# signed binary strings
38+
assert('-0b101'.to_int() == -5, 'Negative binary string conversion failed')
39+
assert('+0b1111'.to_int() == 15, 'Positive binary string conversion failed')
40+
41+
# decimal strings (backwards compat)
42+
assert('10'.to_int() == 10, 'Decimal string conversion failed')
43+
assert('255'.to_int() == 255, 'Decimal string conversion failed')
44+
assert('0'.to_int() == 0, 'Zero decimal conversion failed')
45+
assert('12345'.to_int() == 12345, 'Large decimal conversion failed')
46+
47+
# leading zeroes are decimal (backwards compat)
48+
assert('010'.to_int() == 10, 'Decimal with leading zero broke backward compatibility')
49+
assert('0123'.to_int() == 123, 'Decimal with leading zeros broke backward compatibility')
50+
assert('007'.to_int() == 7, 'Decimal with leading zeros broke backward compatibility')
51+
52+
### .to_string()
53+
54+
# hex format
55+
assert(16.to_string(format: 'hex') == '0x10', 'Int to hex string failed')
56+
assert(255.to_string(format: 'hex') == '0xff', 'Int to hex string failed')
57+
assert(0.to_string(format: 'hex') == '0x0', 'Zero to hex string failed')
58+
assert(1.to_string(format: 'hex') == '0x1', 'One to hex string failed')
59+
assert(3735928559.to_string(format: 'hex') == '0xdeadbeef', 'Large hex string failed')
60+
61+
# octal format
62+
assert(8.to_string(format: 'oct') == '0o10', 'Int to octal string failed')
63+
assert(63.to_string(format: 'oct') == '0o77', 'Int to octal string failed')
64+
assert(493.to_string(format: 'oct') == '0o755', 'Permission to octal string failed')
65+
assert(0.to_string(format: 'oct') == '0o0', 'Zero to octal string failed')
66+
67+
# binary format
68+
assert(2.to_string(format: 'bin') == '0b10', 'Int to binary string failed')
69+
assert(15.to_string(format: 'bin') == '0b1111', 'Int to binary string failed')
70+
assert(255.to_string(format: 'bin') == '0b11111111', 'Byte to binary string failed')
71+
assert(0.to_string(format: 'bin') == '0b0', 'Zero to binary string failed')
72+
73+
# decimal format (explicit)
74+
assert(10.to_string(format: 'dec') == '10', 'Int to decimal string failed')
75+
assert(255.to_string(format: 'dec') == '255', 'Int to decimal string failed')
76+
77+
# default
78+
assert(42.to_string() == '42', 'Default int to string failed')
79+
80+
# fill and hex format
81+
assert(255.to_string(format: 'hex', fill: 8) == '0x0000ff', 'Hex with fill failed')
82+
assert(1.to_string(format: 'hex', fill: 6) == '0x0001', 'Hex with fill failed')
83+
assert(255.to_string(format: 'hex', fill: 4) == '0xff', 'Hex with fill (no padding needed) failed')
84+
85+
# fill and other formats
86+
assert(8.to_string(format: 'oct', fill: 6) == '0o0010', 'Octal with fill failed')
87+
assert(2.to_string(format: 'bin', fill: 10) == '0b00000010', 'Binary with fill failed')
88+
89+
# negative numbers
90+
assert((-15).to_string(format: 'hex') == '-0xf', 'Negative hex conversion failed')
91+
assert((-8).to_string(format: 'oct') == '-0o10', 'Negative octal conversion failed')
92+
assert((-5).to_string(format: 'bin') == '-0b101', 'Negative binary conversion failed')
93+
94+
# negative numbers and fill
95+
assert((-15).to_string(format: 'hex', fill: 6) == '-0x00f', 'Negative hex with fill failed')
96+
assert((-8).to_string(format: 'oct', fill: 7) == '-0o0010', 'Negative octal with fill failed')
97+
assert((-5).to_string(format: 'bin', fill: 8) == '-0b00101', 'Negative binary with fill failed')
98+
assert((-4).to_string(fill: 3) == '-04', 'Negative decimal with fill failed')
99+
100+
# fill and decimal
101+
assert(4.to_string(fill: 3) == '004', 'Decimal with fill failed')
102+
103+
### Round trip conversions
104+
105+
# positive
106+
107+
hex_val = 0x200
108+
hex_str = hex_val.to_string(format: 'hex')
109+
assert(hex_str.to_int() == hex_val, 'Hex round-trip failed')
110+
111+
oct_val = 0o755
112+
oct_str = oct_val.to_string(format: 'oct')
113+
assert(oct_str.to_int() == oct_val, 'Octal round-trip failed')
114+
115+
bin_val = 0b11010
116+
bin_str = bin_val.to_string(format: 'bin')
117+
assert(bin_str.to_int() == bin_val, 'Binary round-trip failed')
118+
119+
# negative
120+
121+
neg_hex = -255
122+
neg_hex_str = neg_hex.to_string(format: 'hex')
123+
assert(neg_hex_str.to_int() == neg_hex, 'Negative hex round-trip failed')
124+
125+
neg_oct = -63
126+
neg_oct_str = neg_oct.to_string(format: 'oct')
127+
assert(neg_oct_str.to_int() == neg_oct, 'Negative octal round-trip failed')
128+
129+
neg_bin = -15
130+
neg_bin_str = neg_bin.to_string(format: 'bin')
131+
assert(neg_bin_str.to_int() == neg_bin, 'Negative binary round-trip failed')

test cases/common/35 string operations/meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ assert('#include <foo/bar.h>'.underscorify() == '_include__foo_bar_h_', 'Broken
5454
assert('Do SomeThing 09'.underscorify() == 'Do_SomeThing_09', 'Broken underscorify')
5555

5656
assert('3'.to_int() == 3, 'String int conversion does not work.')
57+
assert('0x10'.to_int() == 16, 'Hex string conversion does not work.')
58+
assert('0o10'.to_int() == 8, 'Octal string conversion does not work.')
59+
assert('0b10'.to_int() == 2, 'Binary string conversion does not work.')
5760

5861
assert(true.to_string() == 'true', 'bool string conversion failed')
5962
assert(false.to_string() == 'false', 'bool string conversion failed')

0 commit comments

Comments
 (0)