struct2ui 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ """Exporters: turn collected UI values + schema into target file formats.
2
+
3
+ Public API (stable; safe to import from outside):
4
+ emit_c - (sections, registry) -> C source string
5
+ """
6
+
7
+ from .c_emitter import emit_c
8
+ from .json_format import dumps as dumps_json
9
+ from .bin_emitter import emit_bin, merge_abi
10
+ from .elf_verifier import verify_sections
11
+ from .c_parser import parse_c_source, build_schema_dict
12
+
13
+
14
+ __all__ = ['emit_c', 'dumps_json', 'emit_bin', 'merge_abi', 'verify_sections',
15
+ 'parse_c_source', 'build_schema_dict']
@@ -0,0 +1,254 @@
1
+ """JSON/UI values + schema -> raw .bin emitter that mirrors the C memory layout.
2
+
3
+ The output is a byte-for-byte image of the target's struct memory, so the chip
4
+ SDK can memcpy it straight into the algorithm's config struct.
5
+
6
+ Layout model (C ABI simulation):
7
+ - Little-endian.
8
+ - Natural alignment: each member starts at an offset aligned to its
9
+ alignof; gaps are zero padding.
10
+ - A struct's alignof is the max alignof of its members; its size is padded
11
+ up to a multiple of that alignof (tail padding).
12
+ - `pack` is an alignment *cap*: effective align = min(natural, pack).
13
+ - enum -> fixed enum_size bytes (default 4).
14
+ - char[N] / arrays -> fixed length.
15
+
16
+ ABI knobs come from a dict (built-in defaults, overridable by a document-level
17
+ `abi` block). Defaults: little-endian, natural alignment, enum_size=4,
18
+ double 8-aligned, no pack.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
25
+
26
+ from ..schema import (
27
+ Field, ScalarField, EnumField, StructField, ArrayField, SchemaRegistry,
28
+ )
29
+
30
+
31
+ Section = Tuple[str, str, Any] # (label, type_name, values)
32
+
33
+
34
+ # Built-in default ABI: name -> (size, natural_align, struct format char).
35
+ _DEFAULT_TYPES: Dict[str, Tuple[int, int, str]] = {
36
+ 'bool': (1, 1, 'B'), '_bool': (1, 1, 'B'),
37
+ 'char': (1, 1, 'b'),
38
+ 'int8_t': (1, 1, 'b'), 'uint8_t': (1, 1, 'B'),
39
+ 'int16_t': (2, 2, 'h'), 'uint16_t': (2, 2, 'H'),
40
+ 'short': (2, 2, 'h'),
41
+ 'int32_t': (4, 4, 'i'), 'uint32_t': (4, 4, 'I'),
42
+ 'int': (4, 4, 'i'), 'unsigned': (4, 4, 'I'),
43
+ 'long': (4, 4, 'i'),
44
+ 'int64_t': (8, 8, 'q'), 'uint64_t': (8, 8, 'Q'),
45
+ 'float': (4, 4, 'f'), 'double': (8, 8, 'd'),
46
+ }
47
+
48
+ DEFAULT_ABI: Dict[str, Any] = {
49
+ 'endian': 'little',
50
+ 'pack': None,
51
+ 'enum_size': 4,
52
+ 'enum_signed': True,
53
+ 'types': _DEFAULT_TYPES,
54
+ }
55
+
56
+
57
+ def merge_abi(overlay: Optional[Dict[str, Any]]) -> Dict[str, Any]:
58
+ """Merge a document-level abi overlay over the built-in defaults."""
59
+ abi = dict(DEFAULT_ABI)
60
+ abi['types'] = dict(_DEFAULT_TYPES)
61
+ if not overlay:
62
+ return abi
63
+ for key, value in overlay.items():
64
+ if key == 'types' and isinstance(value, dict):
65
+ for tname, spec in value.items():
66
+ abi['types'][tname] = _coerce_type_spec(spec)
67
+ else:
68
+ abi[key] = value
69
+ return abi
70
+
71
+
72
+ def _coerce_type_spec(spec: Any) -> Tuple[int, int, str]:
73
+ if isinstance(spec, (list, tuple)):
74
+ return (int(spec[0]), int(spec[1]), str(spec[2]))
75
+ size = int(spec['size'])
76
+ align = int(spec.get('align', size))
77
+ fmt = str(spec['fmt'])
78
+ return (size, align, fmt)
79
+
80
+
81
+ def emit_bin(sections: Sequence[Section], registry: SchemaRegistry,
82
+ abi: Optional[Dict[str, Any]] = None) -> bytes:
83
+ """Render `sections` as a raw little-endian C-ABI byte image."""
84
+ abi = abi if abi is not None else merge_abi(None)
85
+ out = bytearray()
86
+ for label, type_name, values in sections:
87
+ root = registry.build(type_name)
88
+ _emit_struct(root, values if isinstance(values, dict) else {}, abi, out)
89
+ return bytes(out)
90
+
91
+
92
+ def _endian_prefix(abi: Dict[str, Any]) -> str:
93
+ return '>' if abi.get('endian') == 'big' else '<'
94
+
95
+
96
+ def _cap(natural: int, abi: Dict[str, Any]) -> int:
97
+ pack = abi.get('pack')
98
+ return min(natural, pack) if pack else natural
99
+
100
+
101
+ def _alignof(field: Field, abi: Dict[str, Any]) -> int:
102
+ if isinstance(field, ArrayField):
103
+ return _alignof(field.element, abi)
104
+ if isinstance(field, StructField):
105
+ align = 1
106
+ for child in field.children:
107
+ align = max(align, _alignof(child, abi))
108
+ return _cap(align, abi)
109
+ if isinstance(field, EnumField):
110
+ return _cap(int(abi.get('enum_size', 4)), abi)
111
+ natural = _scalar_size_align(field, abi)[1]
112
+ return _cap(natural, abi)
113
+
114
+
115
+ def _scalar_size_align(field: ScalarField, abi: Dict[str, Any]) -> Tuple[int, int]:
116
+ m = re.match(r'char\[(\d+)\]', field.c_type or '')
117
+ if m:
118
+ return (int(m.group(1)), 1)
119
+ size, align, _ = _lookup_type(field.c_type, abi)
120
+ return (size, align)
121
+
122
+
123
+ def _lookup_type(c_type: str, abi: Dict[str, Any]) -> Tuple[int, int, str]:
124
+ spec = abi['types'].get((c_type or '').lower())
125
+ if spec is None:
126
+ raise ValueError(f'unknown C type for .bin layout: {c_type!r}')
127
+ return spec
128
+
129
+
130
+ def _pad_to(buf: bytearray, align: int) -> None:
131
+ rem = len(buf) % align
132
+ if rem:
133
+ buf.extend(b'\x00' * (align - rem))
134
+
135
+
136
+ def _emit_struct(node: StructField, values: Any, abi: Dict[str, Any],
137
+ out: bytearray) -> None:
138
+ if not isinstance(values, dict):
139
+ values = {}
140
+ start = len(out)
141
+ struct_align = _alignof(node, abi)
142
+ for child in node.children:
143
+ _pad_to_within(out, start, _alignof(child, abi))
144
+ val = values.get(child.name, child.default)
145
+ _emit_field(child, val, abi, out)
146
+ # Tail padding so the struct size is a multiple of its alignment.
147
+ _pad_to_within(out, start, struct_align)
148
+
149
+
150
+ def _pad_to_within(out: bytearray, start: int, align: int) -> None:
151
+ rem = (len(out) - start) % align
152
+ if rem:
153
+ out.extend(b'\x00' * (align - rem))
154
+
155
+
156
+ def _emit_field(field: Field, value: Any, abi: Dict[str, Any],
157
+ out: bytearray) -> None:
158
+ if isinstance(field, ArrayField):
159
+ _emit_array(field, value, abi, out)
160
+ elif isinstance(field, StructField):
161
+ _emit_struct(field, value, abi, out)
162
+ elif isinstance(field, EnumField):
163
+ _emit_enum(field, value, abi, out)
164
+ elif isinstance(field, ScalarField):
165
+ _emit_scalar(field, value, abi, out)
166
+ else:
167
+ raise ValueError(f'cannot serialise field {field!r}')
168
+
169
+
170
+ def _emit_array(field: ArrayField, value: Any, abi: Dict[str, Any],
171
+ out: bytearray) -> None:
172
+ elem = field.element
173
+ items = value if isinstance(value, list) else []
174
+ start = len(out)
175
+ stride = _sizeof(elem, abi)
176
+ for i in range(field.count):
177
+ target = start + i * stride
178
+ if len(out) < target:
179
+ out.extend(b'\x00' * (target - len(out)))
180
+ val = items[i] if i < len(items) else (
181
+ elem.default if elem is not None else None)
182
+ _emit_field(elem, val, abi, out)
183
+ if len(out) < start + (i + 1) * stride:
184
+ out.extend(b'\x00' * (start + (i + 1) * stride - len(out)))
185
+
186
+
187
+ def _emit_enum(field: EnumField, value: Any, abi: Dict[str, Any],
188
+ out: bytearray) -> None:
189
+ num = _enum_to_int(field, value)
190
+ size = int(abi.get('enum_size', 4))
191
+ signed = bool(abi.get('enum_signed', True))
192
+ out.extend(num.to_bytes(size, _byteorder(abi), signed=signed))
193
+
194
+
195
+ def _emit_scalar(field: ScalarField, value: Any, abi: Dict[str, Any],
196
+ out: bytearray) -> None:
197
+ import struct
198
+ m = re.match(r'char\[(\d+)\]', field.c_type or '')
199
+ if m:
200
+ n = int(m.group(1))
201
+ raw = (value if isinstance(value, str) else '').encode('utf-8')[:n]
202
+ out.extend(raw + b'\x00' * (n - len(raw)))
203
+ return
204
+ size, _, fmt = _lookup_type(field.c_type, abi)
205
+ coerced = _coerce_scalar(field.ui_type, value)
206
+ out.extend(struct.pack(_endian_prefix(abi) + fmt, coerced))
207
+
208
+
209
+ def _coerce_scalar(ui_type: str, value: Any) -> Any:
210
+ if ui_type == 'bool':
211
+ return 1 if value else 0
212
+ if ui_type == 'int':
213
+ return int(value if value is not None else 0)
214
+ if ui_type == 'float':
215
+ return float(value if value is not None else 0.0)
216
+ return int(value if value is not None else 0)
217
+
218
+
219
+ def _enum_to_int(field: EnumField, value: Any) -> int:
220
+ if isinstance(value, str):
221
+ if value in field.items:
222
+ return int(field.items[value])
223
+ return 0
224
+ if isinstance(value, bool):
225
+ return int(value)
226
+ if value is None:
227
+ return 0
228
+ return int(value)
229
+
230
+
231
+ def _byteorder(abi: Dict[str, Any]) -> str:
232
+ return 'big' if abi.get('endian') == 'big' else 'little'
233
+
234
+
235
+ def _sizeof(field: Field, abi: Dict[str, Any]) -> int:
236
+ """Aligned size (stride) of a field, including struct/array padding."""
237
+ if isinstance(field, ArrayField):
238
+ return field.count * _sizeof(field.element, abi)
239
+ if isinstance(field, StructField):
240
+ size = 0
241
+ for child in field.children:
242
+ calign = _alignof(child, abi)
243
+ rem = size % calign
244
+ if rem:
245
+ size += calign - rem
246
+ size += _sizeof(child, abi)
247
+ salign = _alignof(field, abi)
248
+ rem = size % salign
249
+ if rem:
250
+ size += salign - rem
251
+ return size
252
+ if isinstance(field, EnumField):
253
+ return int(abi.get('enum_size', 4))
254
+ return _scalar_size_align(field, abi)[0]
@@ -0,0 +1,233 @@
1
+ """JSON/UI values + schema -> C source emitter.
2
+
3
+ Pure functions, no Qt. Input is the collected values (plain dicts/lists, as
4
+ produced by the UI read-back) plus the schema Field tree (from
5
+ SchemaRegistry.build). Output is a C source string.
6
+
7
+ Emit policy (agreed with the project owner):
8
+ - One struct instance per pipeline section.
9
+ - Designated initialisers ({ .field = value }) for struct instances, so the
10
+ output is field-order independent and readable.
11
+ - Flat struct arrays that the UI renders as a *table* use compact positional
12
+ initialisers ({ {50, A_MODE_HIGH}, ... }); other struct arrays keep
13
+ designated initialisers per element.
14
+ - Enum values are written as their symbol (A_MODE_HIGH), not the integer.
15
+ - Booleans -> true/false (assumes <stdbool.h>).
16
+ - Strings (char[N]) -> "quoted".
17
+ - Only .c is produced (no header).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, List, Sequence, Tuple
23
+
24
+ from ..schema import (
25
+ Field, ScalarField, EnumField, StructField, ArrayField, SchemaRegistry,
26
+ )
27
+ from ..ui.tables import is_flat_struct_array
28
+
29
+
30
+ Section = Tuple[str, str, Any] # (variable_label, type_name, values)
31
+
32
+
33
+ def emit_c(sections: Sequence[Section], registry: SchemaRegistry) -> str:
34
+ """Render `sections` as a C source string.
35
+
36
+ sections: ordered list of (label, type_name, values). `label` becomes the
37
+ C variable name, `type_name` is resolved against `registry` to a
38
+ StructField, `values` is the collected per-field dict.
39
+ """
40
+ blocks: List[str] = []
41
+ for label, type_name, values in sections:
42
+ root = registry.build(type_name)
43
+ var = _c_identifier(label)
44
+ init = _emit_struct(root, values, indent=0)
45
+ blocks.append(f'{type_name} {var} = {init};')
46
+
47
+ body = '\n\n'.join(blocks)
48
+ return _HEADER + body + '\n'
49
+
50
+
51
+ _HEADER = (
52
+ '/* Auto-generated by struct2ui. Do not edit by hand. */\n'
53
+ '#include <stdbool.h>\n'
54
+ '#include <stdint.h>\n\n'
55
+ )
56
+
57
+
58
+ def _c_identifier(label: str) -> str:
59
+ out = ''.join(c if c.isalnum() or c == '_' else '_' for c in label)
60
+ if out and out[0].isdigit():
61
+ out = '_' + out
62
+ return out or '_'
63
+
64
+
65
+ def _emit_struct(node: StructField, values: Any, indent: int) -> str:
66
+ """Designated initialiser for a struct: { .a = .., .b = .. }."""
67
+ if not isinstance(values, dict):
68
+ values = {}
69
+ pad = ' ' * (indent + 1)
70
+ close_pad = ' ' * indent
71
+ lines: List[str] = []
72
+ for child in node.children:
73
+ val = values.get(child.name, child.default)
74
+ rendered = _emit_field(child, val, indent + 1)
75
+ lines.append(f'{pad}.{child.name} = {rendered}')
76
+ if not lines:
77
+ return '{ 0 }'
78
+ return '{\n' + ',\n'.join(lines) + f'\n{close_pad}}}'
79
+
80
+
81
+ def _emit_field(field: Field, value: Any, indent: int) -> str:
82
+ if isinstance(field, ArrayField):
83
+ return _emit_array(field, value, indent)
84
+ if isinstance(field, StructField):
85
+ return _emit_struct(field, value, indent)
86
+ if isinstance(field, EnumField):
87
+ return _emit_enum(field, value)
88
+ if isinstance(field, ScalarField):
89
+ return _emit_scalar(field, value)
90
+ return _emit_literal(value)
91
+
92
+
93
+ def _emit_array(field: ArrayField, value: Any, indent: int) -> str:
94
+ elem = field.element
95
+ items = value if isinstance(value, list) else []
96
+
97
+ def at(i: int) -> Any:
98
+ if i < len(items):
99
+ return items[i]
100
+ return elem.default if elem is not None else None
101
+
102
+ # Flat struct array rendered as a table -> compact positional initialiser,
103
+ # one element per line so wide tables stay readable.
104
+ if isinstance(elem, StructField) and _renders_as_table(field):
105
+ pad = ' ' * (indent + 1)
106
+ close_pad = ' ' * indent
107
+ grid: List[List[str]] = []
108
+ for i in range(field.count):
109
+ row = at(i) if isinstance(at(i), dict) else {}
110
+ grid.append([_emit_field(c, row.get(c.name, c.default), indent + 1)
111
+ for c in elem.children])
112
+ widths = _column_widths(grid)
113
+ rows: List[str] = []
114
+ for cells in grid:
115
+ padded = [c.ljust(widths[j]) for j, c in enumerate(cells)]
116
+ rows.append(pad + '{' + ', '.join(padded).rstrip() + '}')
117
+ return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
118
+
119
+ # Other struct array -> designated initialiser per element.
120
+ if isinstance(elem, StructField):
121
+ pad = ' ' * (indent + 1)
122
+ close_pad = ' ' * indent
123
+ rows = [pad + _emit_struct(elem, at(i), indent + 1)
124
+ for i in range(field.count)]
125
+ return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
126
+
127
+ # Scalar/enum array. widget=multiline breaks the values into rows by the
128
+ # innermost dimension; otherwise long arrays are wrapped at MAX_ROW so a
129
+ # single line never runs to hundreds of values. Short arrays stay on one
130
+ # line: { a, b, c }.
131
+ cells = [_emit_field(elem, at(i), indent) for i in range(field.count)]
132
+ row_size = _multiline_row_size(field)
133
+ if row_size > 0:
134
+ return _wrap_cells_aligned(cells, row_size, indent)
135
+ if len(cells) > MAX_ROW:
136
+ return _wrap_cells_aligned(cells, MAX_ROW, indent)
137
+ return '{ ' + ', '.join(cells) + ' }'
138
+
139
+
140
+ # Wrap scalar/enum arrays longer than this many elements, even without an
141
+ # explicit widget=multiline. Keeps generated lines readable.
142
+ MAX_ROW = 64
143
+
144
+
145
+ def _wrap_cells_aligned(cells: List[str], row_size: int, indent: int) -> str:
146
+ """Lay out `cells` row_size-per-line inside braces, padding each column to
147
+ its max width so values line up vertically."""
148
+ pad = ' ' * (indent + 1)
149
+ close_pad = ' ' * indent
150
+ grid = [cells[i:i + row_size] for i in range(0, len(cells), row_size)]
151
+ widths = _column_widths(grid)
152
+ rows = []
153
+ for row in grid:
154
+ padded = [c.ljust(widths[j]) for j, c in enumerate(row)]
155
+ rows.append(pad + ', '.join(padded).rstrip())
156
+ return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
157
+
158
+
159
+ def _column_widths(grid: List[List[str]]) -> List[int]:
160
+ """Max rendered width of each column across all rows (for table alignment)."""
161
+ if not grid:
162
+ return []
163
+ ncols = max(len(row) for row in grid)
164
+ widths = [0] * ncols
165
+ for row in grid:
166
+ for j, cell in enumerate(row):
167
+ if len(cell) > widths[j]:
168
+ widths[j] = len(cell)
169
+ return widths
170
+
171
+
172
+ def _multiline_row_size(field: ArrayField) -> int:
173
+ """Values-per-row for a widget=multiline scalar array.
174
+
175
+ Uses the last (innermost) dimension of meta.shape as the column count so
176
+ a float[3][4] prints 4 numbers per line. Returns 0 when the field is not
177
+ multiline (caller keeps single-line output).
178
+ """
179
+ if field.meta.get('widget') != 'multiline':
180
+ return 0
181
+ shape = field.meta.get('shape')
182
+ if isinstance(shape, (list, tuple)) and shape:
183
+ return int(shape[-1])
184
+ return 0
185
+
186
+
187
+ def _renders_as_table(field: ArrayField) -> bool:
188
+ """Mirror the UI's table-vs-tree decision (ui.renderers).
189
+
190
+ meta.render='tree' forces tree; otherwise a flat struct array is a table.
191
+ Kept in lock-step with TreeRenderer._should_render_as_table so the .c
192
+ layout matches what the user sees.
193
+ """
194
+ if field.meta.get('render') == 'tree':
195
+ return False
196
+ return is_flat_struct_array(field)
197
+
198
+
199
+ def _emit_enum(field: EnumField, value: Any) -> str:
200
+ # Symbol passes through; integer is reverse-looked-up to its symbol.
201
+ if isinstance(value, str) and value in field.items:
202
+ return value
203
+ for symbol, num in field.items.items():
204
+ if num == value:
205
+ return symbol
206
+ if isinstance(value, str) and value:
207
+ return value
208
+ return str(value if value is not None else 0)
209
+
210
+
211
+ def _emit_scalar(field: ScalarField, value: Any) -> str:
212
+ if field.ui_type == 'bool':
213
+ return 'true' if value else 'false'
214
+ if field.ui_type == 'str':
215
+ return _c_string(value if value is not None else '')
216
+ return _emit_literal(value if value is not None else 0)
217
+
218
+
219
+ def _emit_literal(value: Any) -> str:
220
+ if isinstance(value, bool):
221
+ return 'true' if value else 'false'
222
+ if isinstance(value, str):
223
+ return _c_string(value)
224
+ return str(value)
225
+
226
+
227
+ def _c_string(text: str) -> str:
228
+ escaped = (str(text)
229
+ .replace('\\', '\\\\')
230
+ .replace('"', '\\"')
231
+ .replace('\n', '\\n')
232
+ .replace('\t', '\\t'))
233
+ return f'"{escaped}"'