-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgen_cpp_units.py
More file actions
executable file
·303 lines (238 loc) · 10.3 KB
/
gen_cpp_units.py
File metadata and controls
executable file
·303 lines (238 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
#!/usr/bin/env python3
"""
Generate C++ unit wrapper headers from qtty_ffi.h
This script parses the qtty_ffi.h C header file (generated by the Rust qtty-ffi
crate) and automatically generates type-safe C++ wrapper headers for all defined
units. The generator creates:
1. Tag structs: Empty types for template specialization (e.g., MeterTag)
2. UnitTraits: Maps tags to C FFI unit ID constants
3. Type aliases: Convenient names like Meter = Quantity<MeterTag>
4. User-defined literals: Syntax like 10.0_m for intuitive quantity creation
Architecture:
- Input: qtty/qtty-ffi/include/qtty_ffi.h (C header from Rust)
- Output: include/qtty/units/*.hpp (C++ wrappers by dimension)
include/qtty/literals.hpp (user-defined literals)
- Parsing: Regex-based extraction of unit definitions from comments
- Grouping: Discriminant ranges determine dimension (10000-19999 = Length, etc.)
Key features:
- Automatic: No manual editing of generated headers
- Collision detection: Warns about duplicate literal suffixes
- Symbol conversion: Translates unit symbols to valid C++ identifiers
"""
import re
from pathlib import Path
from typing import Dict, List, Tuple
from collections import defaultdict
# File header template
HEADER_TEMPLATE = """#pragma once
#include "../ffi_core.hpp"
namespace qtty {{
{tag_declarations}
{unit_traits}
{type_aliases}
}} // namespace qtty
"""
# Literal file template
LITERALS_HEADER = """#pragma once
#include "units/length.hpp"
#include "units/time.hpp"
#include "units/angular.hpp"
#include "units/mass.hpp"
#include "units/power.hpp"
namespace qtty {{
// Inline namespace for user-defined literals
inline namespace literals {{
{literal_definitions}
}} // namespace literals
}} // namespace qtty
"""
def to_pascal_case(name: str) -> str:
"""Convert UPPER_SNAKE_CASE to PascalCase
Example: KILOMETER -> Kilometer, LIGHT_YEAR -> LightYear
"""
parts = name.split('_')
return ''.join(p.capitalize() for p in parts if p)
def parse_qtty_ffi_header(header_path: Path) -> Dict[str, List[Tuple[str, str, str]]]:
"""Parse qtty_ffi.h and extract unit IDs with symbols grouped by dimension
The qtty_ffi.h header contains unit definitions in a specific comment format:
/* DisplayName (symbol) */ UNIT_ID_NAME = discriminant,
Example:
/* Meter (m) */ UNIT_ID_METER = 10012,
This function:
1. Extracts all unit definitions matching the expected pattern
2. Groups units by dimension based on discriminant range:
- 10000-19999: Length
- 20000-29999: Time
- 30000-39999: Angle
- 40000-49999: Mass
- 50000-59999: Power
3. Returns a dict mapping dimension name to list of (const_name, unit_name, symbol)
Returns:
Dict mapping dimension name -> [(UNIT_ID_NAME, UnitName, "symbol"), ...]
"""
units_by_dimension = defaultdict(list)
with open(header_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract units with their symbols from comments
# Pattern: /* UnitName (symbol) */ UNIT_ID_NAME = discriminant
unit_pattern = r'/\*\s*(\w+)\s*\(([^)]+)\)\s*\*/\s*UNIT_ID_(\w+)\s*=\s*(\d+)'
for match in re.finditer(unit_pattern, content):
display_name = match.group(1)
symbol = match.group(2)
const_name = match.group(3)
discriminant = int(match.group(4))
# Determine dimension from discriminant (first digit)
dim_code = discriminant // 10000
dimension_map = {
1: 'Length',
2: 'Time',
3: 'Angle',
4: 'Mass',
5: 'Power'
}
if dim_code in dimension_map:
dimension = dimension_map[dim_code]
unit_name = to_pascal_case(const_name)
units_by_dimension[dimension].append((const_name, unit_name, symbol))
return dict(units_by_dimension)
def generate_header_for_dimension(dimension: str, units: List[Tuple[str, str, str]]) -> str:
"""Generate a complete header file for a dimension"""
# Generate tag declarations
tag_declarations = []
for _, name, _ in units:
tag_declarations.append(f"struct {name}Tag {{}};")
# Generate unit traits specializations
unit_traits = []
for const_name, name, symbol in units:
unit_traits.append(f"""template<> struct UnitTraits<{name}Tag> {{
static constexpr UnitId unit_id() {{ return UNIT_ID_{const_name}; }}
static constexpr std::string_view symbol() {{ return "{symbol}"; }}
}};""")
# Generate type aliases
type_aliases = []
for _, name, _ in units:
type_aliases.append(f"using {name} = Quantity<{name}Tag>;")
return HEADER_TEMPLATE.format(
tag_declarations='\n'.join(tag_declarations),
unit_traits='\n'.join(unit_traits),
type_aliases='\n'.join(type_aliases)
)
def make_literal_suffix(symbol: str) -> str:
"""Convert a symbol into a valid C++ literal suffix
C++ user-defined literals have strict syntax requirements:
- Must be a valid identifier (alphanumeric + underscore)
- Cannot start with underscore (reserved)
- No special characters except underscore
This function transforms unit symbols to meet these requirements:
- Replace '/' with '_per_' (e.g., 'm/s' -> 'm_per_s')
- Replace special Unicode characters (°, µ, etc.) with ASCII equivalents
- Remove any remaining invalid characters
Returns None if the symbol cannot be made into a valid suffix.
Examples:
'm' -> 'm'
'km/h' -> 'km_per_h'
'°C' -> 'degC'
'µm' -> 'um'
"""
# Replace special characters
suffix = symbol.replace('/', '_per_')
suffix = suffix.replace('°', 'deg')
suffix = suffix.replace('′', 'arcmin')
suffix = suffix.replace('″', 'arcsec')
suffix = suffix.replace('µ', 'u')
suffix = suffix.replace('☉', 'sol')
suffix = suffix.replace('⊕', 'earth')
suffix = suffix.replace('☾', 'moon')
suffix = suffix.replace('♃', 'jupiter')
suffix = suffix.replace(' ', '_')
# Remove any remaining special characters
suffix = re.sub(r'[^a-zA-Z0-9_]', '', suffix)
return suffix if suffix else None
def generate_literals_file(all_units: Dict[str, List[Tuple[str, str, str]]]) -> str:
"""Generate the literals.hpp file for all units with valid symbols
Creates user-defined literal operators for each unit that has a usable symbol.
Each literal gets two overloads: one for long double (123.45_m) and one for
unsigned long long (123_m) to support both floating-point and integer literals.
Collision Detection:
If two units would produce the same literal suffix (e.g., 'nm' for both
Nanometer and NauticalMile), only the first is used and a warning is printed.
Users can still construct these units via their explicit constructors.
Returns:
Complete content of literals.hpp as a string
"""
literal_sections = []
# Track used suffixes to detect collisions
used_suffixes = {}
for dimension in ['Length', 'Time', 'Angle', 'Mass', 'Power']:
if dimension not in all_units:
continue
units = all_units[dimension]
dimension_literals = []
for _, name, symbol in units:
suffix = make_literal_suffix(symbol)
if suffix:
# Check for collision
if suffix in used_suffixes:
# Skip this literal to avoid collision
# Could also use longer suffix like _nm vs _nanometer
print(f" Warning: Skipping literal _{suffix} for {name} (conflicts with {used_suffixes[suffix]})")
continue
used_suffixes[suffix] = name
dimension_literals.append(
f"""constexpr {name} operator""_{suffix}(long double value) {{
return {name}(static_cast<double>(value));
}}
constexpr {name} operator""_{suffix}(unsigned long long value) {{
return {name}(static_cast<double>(value));
}}""")
if dimension_literals:
literal_sections.append(f"// ====================\n// {dimension} literals\n// ====================\n")
literal_sections.append('\n\n'.join(dimension_literals))
return LITERALS_HEADER.format(
literal_definitions='\n\n'.join(literal_sections)
)
def main():
script_dir = Path(__file__).parent
header_path = script_dir / 'qtty' / 'qtty-ffi' / 'include' / 'qtty_ffi.h'
include_dir = script_dir / 'include' / 'qtty' / 'units'
if not header_path.exists():
print(f"Error: {header_path} not found")
print("Make sure the qtty submodule is built (cargo build -p qtty-ffi)")
return 1
print(f"Reading units from: {header_path}")
units_by_dimension = parse_qtty_ffi_header(header_path)
# Create include directory if it doesn't exist
include_dir.mkdir(parents=True, exist_ok=True)
# Mapping of dimension names to file names
dimension_files = {
'Length': 'length.hpp',
'Time': 'time.hpp',
'Angle': 'angular.hpp',
'Mass': 'mass.hpp',
'Power': 'power.hpp',
}
# Generate header for each dimension
for dimension, filename in dimension_files.items():
if dimension in units_by_dimension:
units = units_by_dimension[dimension]
header_content = generate_header_for_dimension(dimension, units)
output_path = include_dir / filename
with open(output_path, 'w', encoding='utf-8') as f:
f.write(header_content)
print(f"Generated {filename} with {len(units)} units")
else:
print(f"Warning: No units found for dimension {dimension}")
# Generate literals.hpp
literals_path = script_dir / 'include' / 'qtty' / 'literals.hpp'
literals_content = generate_literals_file(units_by_dimension)
with open(literals_path, 'w', encoding='utf-8') as f:
f.write(literals_content)
print(f"Generated literals.hpp")
# Print summary
total_units = sum(len(units) for units in units_by_dimension.values())
print(f"\nTotal units generated: {total_units}")
for dimension, units in sorted(units_by_dimension.items()):
print(f" {dimension}: {len(units)} units")
return 0
if __name__ == '__main__':
exit(main())