Skip to content

Commit 2450c75

Browse files
committed
fix(python): extend CF_RULE struct layout to match fm_cf_rule_t
The Python wheel drives formulon_capi.wasm through wasmtime using a hand-mirrored byte layout of the C ABI structs. CF_RULE in packages/python/formulon/_structs.py was defined before fm_cf_rule_t gained ColorScale/DataBar/IconSet fields, leaving the Python struct at 88 bytes against the real 168-byte C struct. alloc_struct(LIB, S.CF_RULE) therefore allocated a WASM-linear-memory buffer 80 bytes too small for what fm_sheet_cf_add_rule/fm_sheet_cf_get_at actually read/write, corrupting the round-tripped type field (and potentially nearby WASM heap state) and failing test_cf_add_get_evaluate_clear ("0 != 1"). - Add CFVO_BLOB and CF_COLOR_BLOB opaque blob primitives, following the existing VALUE_BLOB precedent, for the by-value-embedded fm_cfvo_t and fm_cf_color_t sub-structs - Generalize Struct.unpack()'s opaque-blob skip from the hardcoded "blob16" check to "kind not in _FMT" so future blob kinds don't need another hardcoded name - Extend CF_RULE's field list with every field fm_cf_rule_t gained: color_scale_thresholds/colors/count, data_bar_engaged, data_bar_min/max (CFVO_BLOB), data_bar_fill (CF_COLOR_BLOB), data_bar_show_value, data_bar_min/max_length_pct, icon_set_engaged, icon_set_name, icon_set_thresholds, icon_set_threshold_count, icon_set_reverse, icon_set_show_value, icon_set_percent, in the declared order of src/c_api/formulon_c.h. Struct's existing alignment-rounding reproduces the C compiler's inter-field padding automatically; total size now correctly computes to 168 bytes - Update the stale EXPECTED_SIZES["CF_RULE"] drift guard in packages/python/tests/test_surface.py from 88 to 168 Testing: - make python-test: 52/52 pass, including test_cf_add_get_evaluate_clear and test_struct_sizes - make python-wheel followed by installing the built wheel into a fresh venv and running the test suite against it (the same path release.yml's smoke test step runs): 52/52 pass - Native ctest -LE "SLOW|LOAD|BENCH": 14929/14929 pass, confirming no native regression from this Python-only change
1 parent 56b23b8 commit 2450c75

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

packages/python/formulon/_structs.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
# decoded by :meth:`Struct.unpack`; callers read it via Value._from_wasm
3838
# against ``ptr + offset``.
3939
VALUE_BLOB = ("blob16", 16, 8)
40+
# Opaque 12-byte, 4-aligned blob: an inline ``fm_cfvo_t`` (``uint8_t type;
41+
# uint8_t _pad[3]; int32_t gte; const char* value;``). Not decoded by
42+
# :meth:`Struct.unpack`; callers who need the sub-fields read them at
43+
# ``ptr + offset`` with their own struct.unpack_from calls.
44+
CFVO_BLOB = ("blob_cfvo", 12, 4)
45+
# Opaque 4-byte, 1-aligned blob: an inline ``fm_cf_color_t`` (four
46+
# ``uint8_t`` channels). Not decoded by :meth:`Struct.unpack`.
47+
CF_COLOR_BLOB = ("blob_cf_color", 4, 1)
4048

4149
_FMT = {
4250
"ptr": "<I",
@@ -77,13 +85,14 @@ def pack(self, lib, ptr: int, values: Dict[str, object]) -> None:
7785
def unpack(self, lib, ptr: int) -> Dict[str, int]:
7886
"""Read every scalar field from ``[ptr, ptr+size)`` into a dict.
7987
80-
Opaque ``blob16`` fields (inline ``fm_value_t``) are skipped; read
81-
them separately via ``Value._from_wasm(ptr + self.offsets[name][1])``.
88+
Opaque blob fields (any ``kind`` not in ``_FMT``, e.g. an inline
89+
``fm_value_t`` or ``fm_cfvo_t``) are skipped; callers decode those
90+
separately against ``ptr + self.offsets[name][1]``.
8291
"""
8392
raw = lib.read_bytes(ptr, self.size)
8493
out: Dict[str, int] = {}
8594
for fname, (kind, off) in self.offsets.items():
86-
if kind == "blob16":
95+
if kind not in _FMT:
8796
continue
8897
out[fname] = struct.unpack_from(_FMT[kind], raw, off)[0]
8998
return out
@@ -248,6 +257,23 @@ def alloc_struct(lib, layout: Struct) -> int:
248257
("std_dev", F64),
249258
("text", PTR),
250259
("time_period_engaged", I32),
260+
("color_scale_thresholds", PTR),
261+
("color_scale_colors", PTR),
262+
("color_scale_count", U32),
263+
("data_bar_engaged", I32),
264+
("data_bar_min", CFVO_BLOB),
265+
("data_bar_max", CFVO_BLOB),
266+
("data_bar_fill", CF_COLOR_BLOB),
267+
("data_bar_show_value", I32),
268+
("data_bar_min_length_pct", U8),
269+
("data_bar_max_length_pct", U8),
270+
("icon_set_engaged", I32),
271+
("icon_set_name", U8),
272+
("icon_set_thresholds", PTR),
273+
("icon_set_threshold_count", U32),
274+
("icon_set_reverse", I32),
275+
("icon_set_show_value", I32),
276+
("icon_set_percent", I32),
251277
],
252278
)
253279

packages/python/tests/test_surface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class StructLayoutTests(unittest.TestCase):
4545
"SHEET_PROTECTION": 88,
4646
"VIEWPORT": 20,
4747
"CF_MATCH": 72,
48-
"CF_RULE": 88,
48+
"CF_RULE": 168,
4949
"PIVOT_CELL": 40,
5050
"PIVOT_FIELD_SPEC": 20,
5151
"PIVOT_DATA_FIELD_SPEC": 28,

0 commit comments

Comments
 (0)