From d50996ec59a8cb46772ab018dbf146a4f2fdd459 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Fri, 10 Jul 2026 15:18:16 +0200 Subject: [PATCH 1/3] descriptor: optional fusion mag input Signed-off-by: raiden00pl --- src/dawnpy/descriptor/handlers/prog_fusion.py | 49 +++++++++++++++++-- tests/descriptor/handlers/test_prog_fusion.py | 47 ++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/dawnpy/descriptor/handlers/prog_fusion.py b/src/dawnpy/descriptor/handlers/prog_fusion.py index 1e0f412..57454f7 100644 --- a/src/dawnpy/descriptor/handlers/prog_fusion.py +++ b/src/dawnpy/descriptor/handlers/prog_fusion.py @@ -7,9 +7,14 @@ from typing import Any +from dawnpy.descriptor.config_access import config_field_is_rw from dawnpy.descriptor.definitions.type_info import ConfigField -from dawnpy.descriptor.encoding.scalar import encode_scalar_words +from dawnpy.descriptor.encoding.scalar import ( + encode_scalar_words, + format_scalar_cpp, +) from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx from dawnpy.descriptor.support.utils import resolve_reference from dawnpy.headerdefs.bundle import header_cfg_id @@ -36,6 +41,9 @@ ("output", "cfgIdOutput"), ) +#: Optional single-id fields (encoded only when present in the config). +_OPTIONAL_ID_FIELDS: tuple[tuple[str, str], ...] = (("mag", "cfgIdMag"),) + def config_fields() -> list[ConfigField]: # pragma: no cover """Return the user-facing YAML config schema for ``fusion``.""" @@ -50,6 +58,11 @@ def config_fields() -> list[ConfigField]: # pragma: no cover cpp_helper=f"{cpp_class}::cfgIdGyro", value_type="id_single", ), + ConfigField( + name="mag", + cpp_helper=f"{cpp_class}::cfgIdMag", + value_type="id_single", + ), ConfigField( name="output", cpp_helper=f"{cpp_class}::cfgIdOutput", @@ -80,6 +93,36 @@ def params_words(config: dict[str, Any]) -> list[int]: return words +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: Any, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``fusion`` params block; return whether handled.""" + if field_def.value_type != "fusion_params": + return False + + params = config.get(field_def.name, {}) + if not isinstance(params, dict): + params = {} + + # rw is true only when a writable config IO targets these params; the + # config IO's reference emits the same cfgParams(rw) so the runtime + # cfg-id lookup matches. + rw = config_field_is_rw(ctx.rw_grants, obj.obj_id, field_def.name) + rw_arg = "true" if rw else "" + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({rw_arg})," + ) + for name in PARAM_ORDER: + raw = params.get(name, PARAM_DEFAULTS[name]) + for literal in format_scalar_cpp(raw, "float"): + ctx.format_helper.append_line(lines, 3, f"{literal},") + return True + + def validate_object(obj: Any) -> list[str]: """Require the accel/gyro/output references.""" config = obj.config if isinstance(obj.config, dict) else {} @@ -94,7 +137,7 @@ def validate_object_refs(obj: Any, io_map: dict[str, Any]) -> list[str]: """All bound IOs must be float.""" config = obj.config if isinstance(obj.config, dict) else {} errors: list[str] = [] - for name, _ in _ID_FIELDS: + for name, _ in _ID_FIELDS + _OPTIONAL_ID_FIELDS: ref = resolve_reference(config.get(name)) io = io_map.get(ref) if ref else None if io is not None and io.dtype != "float": @@ -122,7 +165,7 @@ def encode_binary( del decoder config = obj.config if isinstance(obj.config, dict) else {} - for name, helper in _ID_FIELDS: + for name, helper in _ID_FIELDS + _OPTIONAL_ID_FIELDS: ref = resolve_reference(config.get(name)) obj_id = obj_ids.get(ref, 0) if ref else 0 if obj_id: diff --git a/tests/descriptor/handlers/test_prog_fusion.py b/tests/descriptor/handlers/test_prog_fusion.py index 1fa5dbd..35938b3 100644 --- a/tests/descriptor/handlers/test_prog_fusion.py +++ b/tests/descriptor/handlers/test_prog_fusion.py @@ -135,6 +135,53 @@ def test_output_shape_ownership(self): obj = _obj(config={"accel": "accel0", "gyro": "gyro0"}) assert output_shape_owned_virt_targets(obj) == set() + def test_encode_binary_with_mag(self): + items = [] + obj = _obj( + config={ + "accel": "accel0", + "gyro": "gyro0", + "mag": "mag0", + "output": "fused_src", + } + ) + + encode_binary( + items, + obj, + 34, + {"accel0": 1, "gyro0": 2, "mag0": 4, "fused_src": 3}, + None, + ) + + # accel, gyro, output, mag ids + params. + assert len(items) == 5 + assert items[3][1] == [4] + + def test_validate_mag_dtype(self): + io_map = { + "accel0": to_io_obj({"dtype": "float"}, "accel0"), + "gyro0": to_io_obj({"dtype": "float"}, "gyro0"), + "mag0": to_io_obj({"dtype": "int16"}, "mag0"), + "fused_src": to_io_obj({"dtype": "float"}, "fused_src"), + } + obj = _obj( + config={ + "accel": "accel0", + "gyro": "gyro0", + "mag": "mag0", + "output": "fused_src", + } + ) + + errors = validate_object_refs(obj, io_map) + + assert len(errors) == 1 + assert "mag0" in errors[0] + + def test_mag_is_optional(self): + assert validate_object(_obj()) == [] + def test_emit_fusion_params(self): from dawnpy.descriptor.definitions.loader import ConfigLoader From 9a2e318fc92d93cfb76236314887097840b00c61 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Fri, 10 Jul 2026 15:59:19 +0200 Subject: [PATCH 2/3] descriptor: move program C++ config emission into handlers Signed-off-by: raiden00pl --- src/dawnpy/descriptor/generation/prog.py | 440 ++---------------- src/dawnpy/descriptor/handlers/_base.py | 34 ++ .../descriptor/handlers/_prog_config_cpp.py | 237 ++++++++++ src/dawnpy/descriptor/handlers/prog_adjust.py | 41 +- .../descriptor/handlers/prog_bitpack.py | 28 ++ src/dawnpy/descriptor/handlers/prog_buffer.py | 43 ++ .../descriptor/handlers/prog_gateway.py | 44 ++ .../descriptor/handlers/prog_sequencer.py | 36 ++ src/dawnpy/descriptor/handlers/prog_switch.py | 40 ++ tests/descriptor/handlers/helpers.py | 8 + tests/descriptor/handlers/test_prog_adjust.py | 186 ++++---- .../descriptor/handlers/test_prog_bitpack.py | 41 ++ tests/descriptor/handlers/test_prog_buffer.py | 65 +++ tests/descriptor/handlers/test_prog_common.py | 307 +----------- .../handlers/test_prog_config_cpp.py | 337 ++++++++++++++ tests/descriptor/handlers/test_prog_fusion.py | 58 ++- .../descriptor/handlers/test_prog_gateway.py | 82 ++-- .../handlers/test_prog_sequencer.py | 91 +++- tests/descriptor/handlers/test_prog_switch.py | 75 +++ 19 files changed, 1331 insertions(+), 862 deletions(-) create mode 100644 src/dawnpy/descriptor/handlers/_prog_config_cpp.py create mode 100644 tests/descriptor/handlers/test_prog_bitpack.py create mode 100644 tests/descriptor/handlers/test_prog_buffer.py create mode 100644 tests/descriptor/handlers/test_prog_config_cpp.py create mode 100644 tests/descriptor/handlers/test_prog_switch.py diff --git a/src/dawnpy/descriptor/generation/prog.py b/src/dawnpy/descriptor/generation/prog.py index 8ebf7a7..29c206f 100644 --- a/src/dawnpy/descriptor/generation/prog.py +++ b/src/dawnpy/descriptor/generation/prog.py @@ -1,43 +1,37 @@ -# tools/dawnpy/src/dawnpy/descriptor/prog_generators.py +# tools/dawnpy/src/dawnpy/descriptor/generation/prog.py # # SPDX-License-Identifier: Apache-2.0 # -"""Program-specific descriptor generation helpers.""" +"""Program descriptor C++ generation orchestrator. + +Each program's type-specific config emission lives in its handler +(``handlers/prog_*.py``), symmetric with the binary ``encode_binary`` path. +This module only drives the shared per-object structure: the config-count +header, the iobind item, and the delegation to the handler's config emitter. +""" from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING, Any -from dawnpy.descriptor.config_access import ( - ConfigRwGrants, - config_field_is_rw, -) -from dawnpy.descriptor.definitions.type_info import ConfigField -from dawnpy.descriptor.encoding.scalar import format_scalar_cpp +from dawnpy.descriptor.config_access import ConfigRwGrants from dawnpy.descriptor.handlers import PROG_HANDLER_REGISTRY +from dawnpy.descriptor.handlers._prog_config_cpp import ( + ProgFieldCppCtx, + emit_config_fields_cpp, +) from dawnpy.descriptor.support.formatting import DescriptorFormatHelper -from dawnpy.descriptor.support.utils import resolve_reference if TYPE_CHECKING: from dawnpy.descriptor.definitions.objects import ProgramObject - -def _resolve_id(ref: Any) -> str | None: # pragma: no cover - """Resolve a YAML anchor or string reference to an object ID.""" - if isinstance(ref, dict): - return ref.get("id") # pragma: no cover - if ref is not None: - return str(ref) # pragma: no cover - return None # pragma: no cover - - -def _resolve_ids(refs: Any) -> list[str]: # pragma: no cover - """Resolve a list of YAML references to object ID strings.""" - if not isinstance(refs, list): - return [] # pragma: no cover - return [r for r in (_resolve_id(ref) for ref in refs) if r] +#: Value-types whose handler emits its own iobind item, replacing the +#: standard interleaved (source, output) iobind pairs. +_CUSTOM_IOBIND_VALUE_TYPES = frozenset( + {"id_array_pairs", "gateway_iobind", "id_array_quads", "id_list"} +) class ProgramConfigGenerator: @@ -57,377 +51,6 @@ def __init__( self._format_helper = format_helper or DescriptorFormatHelper() self._config_rw_grants = config_rw_grants or (lambda: {}) - def _emit_id_array_pairs( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - obj: ProgramObject, - config: dict[str, Any], - ) -> None: - sources = _resolve_ids(config.get("sources", obj.inputs)) - outputs = _resolve_ids(config.get("outputs", obj.outputs)) - n = len(sources) + len(outputs) - if len(sources) != len(outputs): - raise ValueError( - f"Program {obj.obj_id} has {len(sources)} sources and " - f"{len(outputs)} outputs" - ) - self._format_helper.append_line(lines, 2, f"{cpp_helper}({n}),") - for src_id, output_id in zip(sources, outputs, strict=True): - self._format_helper.append_line(lines, 3, f"{src_id.upper()},") - self._format_helper.append_line(lines, 3, f"{output_id.upper()},") - - def _emit_uint32( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - default: str = "", - ) -> None: - value = config.get(field_name, default if default else 0) - self._format_helper.append_line(lines, 2, f"{cpp_helper}(),") - self._format_helper.append_line(lines, 3, f"{value},") - - def _emit_uint32_list( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - values = config.get(field_name, []) - n = len(values) - self._format_helper.append_line(lines, 2, f"{cpp_helper}({n}),") - for v in values: - self._format_helper.append_line(lines, 3, f"{int(v)},") - - def _emit_id_array( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - obj: ProgramObject, - ) -> None: - self._format_helper.append_line(lines, 2, f"{cpp_helper}(),") - # For standard inputs/outputs, they are in obj.inputs/obj.outputs - if field_name == "inputs": # pragma: no cover - ids = obj.inputs - elif field_name == "outputs": # pragma: no cover - ids = obj.outputs - else: # pragma: no cover - ids = obj.config.get(field_name, []) - - for obj_id in ids: - self._format_helper.append_line(lines, 3, f"{obj_id.upper()},") - - def _emit_id_list( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - ids = _resolve_ids(config.get(field_name, [])) - self._format_helper.append_line(lines, 2, f"{cpp_helper}({len(ids)}),") - for obj_id in ids: - self._format_helper.append_line(lines, 3, f"{obj_id.upper()},") - - def _emit_id_single( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - obj: ProgramObject, - ) -> None: - if field_name == "reset": - obj_id = obj.reset - else: - obj_id = obj.config.get(field_name) - if isinstance(obj_id, list): - obj_id = obj_id[0] if obj_id else None - obj_id = resolve_reference(obj_id) if obj_id else None - - self._format_helper.append_line(lines, 2, f"{cpp_helper}(),") - if obj_id: - self._format_helper.append_line(lines, 3, f"{obj_id.upper()},") - else: - self._format_helper.append_line(lines, 3, "0,") - - def _emit_gateway_iobind( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - entries = config.get(field_name, []) - if not isinstance(entries, list): # pragma: no cover - entries = [] - - resolved_gateway: list[tuple[str, str, int, int]] = [] - for entry in entries: - if not isinstance(entry, dict): # pragma: no cover - continue - io1 = _resolve_id(entry.get("io1")) - io2 = _resolve_id(entry.get("io2")) - if not io1 or not io2: # pragma: no cover - continue - flags = int(entry.get("flags", 0)) - dim = int(entry.get("dim", 1)) - resolved_gateway.append((io1, io2, flags, dim)) - - self._format_helper.append_line( - lines, 2, f"{cpp_helper}({4 * len(resolved_gateway)})," - ) - for io1, io2, flags, dim in resolved_gateway: - self._format_helper.append_line(lines, 3, f"{io1.upper()},") - self._format_helper.append_line(lines, 3, f"{io2.upper()},") - self._format_helper.append_line(lines, 3, f"{flags},") - self._format_helper.append_line(lines, 3, f"{dim},") - - def _emit_id_array_quads( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - entries = config.get(field_name, []) - if not isinstance(entries, list): # pragma: no cover - entries = [] - - resolved_quads: list[tuple[str, str, str, str]] = [] - for entry in entries: - if not isinstance(entry, dict): # pragma: no cover - continue - src = _resolve_id(entry.get("src")) - out = _resolve_id(entry.get("out")) - sel = _resolve_id(entry.get("sel")) - stat = _resolve_id(entry.get("stat")) - if not src or not out or not sel or not stat: # pragma: no cover - continue - resolved_quads.append((src, out, sel, stat)) - - size = 4 * len(resolved_quads) - self._format_helper.append_line(lines, 2, f"{cpp_helper}({size}),") - for src, out, sel, stat in resolved_quads: - self._format_helper.append_line(lines, 3, f"{src.upper()},") - self._format_helper.append_line(lines, 3, f"{out.upper()},") - self._format_helper.append_line(lines, 3, f"{sel.upper()},") - self._format_helper.append_line(lines, 3, f"{stat.upper()},") - - def _emit_adjust_params( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - obj: ProgramObject, - config: dict[str, Any], - ) -> None: - params = config.get(field_name, {}) - if not isinstance(params, dict): # pragma: no cover - params = {} - - # rw is true only when a writable config IO targets these params; the - # config IO's reference emits the same cfgParams(rw) so the runtime - # cfg-id lookup matches. - rw = config_field_is_rw( - self._config_rw_grants(), obj.obj_id, field_name - ) - rw_arg = "true" if rw else "" - self._format_helper.append_line(lines, 2, f"{cpp_helper}({rw_arg}),") - for raw in (params.get("offset", 0), params.get("scale", 1)): - for literal in format_scalar_cpp(raw, obj.dtype): - self._format_helper.append_line(lines, 3, f"{literal},") - - def _emit_fusion_params( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - obj: ProgramObject, - config: dict[str, Any], - ) -> None: - from dawnpy.descriptor.handlers.prog_fusion import ( - PARAM_DEFAULTS, - PARAM_ORDER, - ) - - params = config.get(field_name, {}) - if not isinstance(params, dict): # pragma: no cover - params = {} - - # rw is true only when a writable config IO targets these params; the - # config IO's reference emits the same cfgParams(rw) so the runtime - # cfg-id lookup matches. - rw = config_field_is_rw( - self._config_rw_grants(), obj.obj_id, field_name - ) - rw_arg = "true" if rw else "" - self._format_helper.append_line(lines, 2, f"{cpp_helper}({rw_arg}),") - for name in PARAM_ORDER: - raw = params.get(name, PARAM_DEFAULTS[name]) - for literal in format_scalar_cpp(raw, "float"): - self._format_helper.append_line(lines, 3, f"{literal},") - - def _emit_adjust_iobind( # pragma: no cover - self, - lines: list[str], - obj: ProgramObject, - ) -> None: - """Compatibility wrapper for the adjust handler iobind emitter.""" - handler = PROG_HANDLER_REGISTRY["adjust"] - handler.emit_iobind_cpp( - lines, - obj, - len(obj.inputs) + len(obj.outputs), - self._format_helper, - "CProgAdjust", - ) - - def _emit_sequencer_states( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - entries = config.get(field_name, []) - encoded: list[tuple[int, int]] = [] - entry: Any - - if not isinstance(entries, list): # pragma: no cover - entries = [] - - for entry in entries: - value_raw: Any - dwell_raw: Any - value: int - dwell: int - - if not isinstance(entry, dict): # pragma: no cover - continue - - value_raw = entry.get("value", 0) - dwell_raw = entry.get("dwell_us", 0) - value = int(value_raw) - dwell = int(dwell_raw) - encoded.append((value, dwell)) - - self._format_helper.append_line( - lines, 2, f"{cpp_helper}({2 * len(encoded)})," - ) - for value, dwell in encoded: - self._format_helper.append_line(lines, 3, f"{value},") - self._format_helper.append_line(lines, 3, f"{dwell},") - - def _emit_switch_inputs( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - entries = config.get(field_name, []) - words = [] - if isinstance(entries, list): - for e in entries: - if isinstance(e, dict): - io = resolve_reference(e.get("io", "")) - words.append(io.upper() if io else "0") - words.append(str(int(e.get("match", 1)))) - self._format_helper.append_line( - lines, 2, f"{cpp_helper}({len(words)})," - ) - for w in words: - self._format_helper.append_line(lines, 3, f"{w},") - - def _emit_switch_target( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - target = config.get(field_name, []) - tgt_ref = resolve_reference(target[0]) if target else "" - tgt_id = tgt_ref.upper() if tgt_ref else "0" - on_cmd = str(int(target[1])) if len(target) > 1 else "1" - off_cmd = str(int(target[2])) if len(target) > 2 else "0" - self._format_helper.append_line(lines, 2, f"{cpp_helper}(),") - self._format_helper.append_line(lines, 3, f"{tgt_id},") - self._format_helper.append_line(lines, 3, f"{on_cmd},") - self._format_helper.append_line(lines, 3, f"{off_cmd},") - - def _emit_bitpack_inputs( # pragma: no cover - self, - lines: list[str], - cpp_helper: str, - field_name: str, - config: dict[str, Any], - ) -> None: - entries = config.get(field_name, []) - words = [] - if isinstance(entries, list): - for e in entries: - if isinstance(e, dict): - io = resolve_reference(e.get("io", "")) - words.append(io.upper() if io else "0") - words.append(str(int(e.get("bit", 0)))) - self._format_helper.append_line( - lines, 2, f"{cpp_helper}({len(words)})," - ) - for w in words: - self._format_helper.append_line(lines, 3, f"{w},") - - def _emit_type_field( # noqa: C901 # pragma: no cover - self, - lines: list[str], - field_def: ConfigField, - obj: ProgramObject, - config: dict[str, Any], - ) -> None: - """Emit descriptor lines for one type-specific config field.""" - field_name = field_def.name - value_type = field_def.value_type - cpp_helper = field_def.cpp_helper - - if value_type == "id_array_pairs": - self._emit_id_array_pairs(lines, cpp_helper, obj, config) - elif value_type == "uint32": - self._emit_uint32( - lines, cpp_helper, field_name, config, field_def.default - ) - elif value_type == "uint32_list": - self._emit_uint32_list(lines, cpp_helper, field_name, config) - elif value_type == "id_array": - self._emit_id_array(lines, cpp_helper, field_name, obj) - elif value_type == "id_list": - self._emit_id_list(lines, cpp_helper, field_name, config) - elif value_type == "id_single": - self._emit_id_single(lines, cpp_helper, field_name, obj) - elif value_type == "gateway_iobind": - self._emit_gateway_iobind(lines, cpp_helper, field_name, config) - elif value_type == "id_array_quads": - self._emit_id_array_quads(lines, cpp_helper, field_name, config) - elif value_type == "adjust_params": - self._emit_adjust_params( - lines, cpp_helper, field_name, obj, config - ) - elif value_type == "fusion_params": - self._emit_fusion_params( - lines, cpp_helper, field_name, obj, config - ) - elif value_type == "sequencer_states": - self._emit_sequencer_states(lines, cpp_helper, field_name, config) - elif value_type == "switch_inputs": - self._emit_switch_inputs(lines, cpp_helper, field_name, config) - elif value_type == "switch_target": - self._emit_switch_target(lines, cpp_helper, field_name, config) - elif value_type == "bitpack_inputs": - self._emit_bitpack_inputs(lines, cpp_helper, field_name, config) - def generate_prog_config( # pragma: no cover self, macro_name: str, obj: ProgramObject ) -> list[str]: @@ -445,14 +68,7 @@ def generate_prog_config( # pragma: no cover # Type-specific custom iobind field replaces the standard iobind item has_custom_iobind = any( - f.value_type - in { - "id_array_pairs", - "gateway_iobind", - "id_array_quads", - "id_list", - } - for f in type_fields + f.value_type in _CUSTOM_IOBIND_VALUE_TYPES for f in type_fields ) # Compute total number of config items @@ -486,8 +102,20 @@ def generate_prog_config( # pragma: no cover self._format_helper.append_line(lines, 3, f"{src.upper()},") self._format_helper.append_line(lines, 3, f"{dst.upper()},") - # Process type-specific config items (each is a separate config entry) - for field_def in type_fields: - self._emit_type_field(lines, field_def, obj, config) + # Type-specific config items are owned by the program's handler; the + # handler's per-field hook covers program-specific value-types and the + # shared generic emitters cover the rest. A program type with config + # fields but no handler module (e.g. OOT types using only generic + # value-types) emits through the generic path directly. + if type_fields: + ctx = ProgFieldCppCtx( + self._format_helper, self._config_rw_grants() + ) + if handler is not None: + handler.emit_config_cpp(lines, obj, config, type_fields, ctx) + else: + emit_config_fields_cpp( + None, prog_type, lines, obj, config, type_fields, ctx + ) return lines diff --git a/src/dawnpy/descriptor/handlers/_base.py b/src/dawnpy/descriptor/handlers/_base.py index 16a6f70..5b7e3db 100644 --- a/src/dawnpy/descriptor/handlers/_base.py +++ b/src/dawnpy/descriptor/handlers/_base.py @@ -17,6 +17,10 @@ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers._prog_config_cpp import ( + ProgFieldCppCtx, + emit_config_fields_cpp, +) if TYPE_CHECKING: from dawnpy.descriptor.client import ClientIo, ClientProgram @@ -157,6 +161,17 @@ def emit_iobind_cpp( """Emit custom C++ iobind config and return whether handled.""" raise NotImplementedError + def emit_config_cpp( + self, + lines: list[str], + obj: ProgramObject, + config: dict[str, Any], + field_defs: list[ConfigField], + ctx: ProgFieldCppCtx, + ) -> None: + """Emit C++ source for this program's type-specific config fields.""" + raise NotImplementedError + @runtime_checkable class ProtoHandler(Protocol): @@ -488,6 +503,25 @@ def emit_iobind_cpp( return False return bool(custom(lines, obj, total_ids, format_helper, cpp_class)) + def emit_config_cpp( + self, + lines: list[str], + obj: ProgramObject, + config: dict[str, Any], + field_defs: list[ConfigField], + ctx: ProgFieldCppCtx, + ) -> None: + """Emit C++ source for this program's type-specific config fields. + + Delegates to the shared field loop: the handler's own + ``emit_config_field_cpp`` hook owns program-specific value-types; + anything it declines falls back to the shared generic emitters. + """ + hook = getattr(self._module, "emit_config_field_cpp", None) + emit_config_fields_cpp( + hook, self.yaml_type, lines, obj, config, field_defs, ctx + ) + class ProtoHandlerAdapter(ModuleHandlerAdapter): """Default PROTO policy plus module-handler delegation.""" diff --git a/src/dawnpy/descriptor/handlers/_prog_config_cpp.py b/src/dawnpy/descriptor/handlers/_prog_config_cpp.py new file mode 100644 index 0000000..7499124 --- /dev/null +++ b/src/dawnpy/descriptor/handlers/_prog_config_cpp.py @@ -0,0 +1,237 @@ +# tools/dawnpy/src/dawnpy/descriptor/handlers/_prog_config_cpp.py +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""Shared C++ source emission for program config fields. + +The C++ source-text config path is per-handler, symmetric with the binary +``encode_binary`` path: each program handler owns emission of its own +program-specific fields via an ``emit_config_field_cpp`` module hook. The +generic value-types shared by many programs (``id_single``, ``id_list``, +``uint32``, ...) live here as free functions and are dispatched by +:func:`emit_generic_config_field`, which every handler's default emission +loop falls back to. +""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from dawnpy.descriptor.config_access import ConfigRwGrants +from dawnpy.descriptor.support.utils import resolve_reference + +if TYPE_CHECKING: + from dawnpy.descriptor.definitions.objects import ProgramObject + from dawnpy.descriptor.definitions.type_info import ConfigField + from dawnpy.descriptor.support.formatting import DescriptorFormatHelper + + +@dataclass +class ProgFieldCppCtx: + """Ambient state a program config-field emitter needs. + + :ivar format_helper: Emits indented C++ source lines. + :ivar rw_grants: Resolved ``(obj_id, field_name) -> rw`` write grants so + rw-aware fields (``adjust``/``fusion`` params) emit the same rw flag + the referencing config IO does. + """ + + format_helper: DescriptorFormatHelper + rw_grants: ConfigRwGrants + + +def resolve_id(ref: Any) -> str | None: + """Resolve a YAML anchor or string reference to an object ID.""" + if isinstance(ref, dict): + return ref.get("id") + if ref is not None: + return str(ref) + return None + + +def resolve_ids(refs: Any) -> list[str]: + """Resolve a list of YAML references to object ID strings.""" + if not isinstance(refs, list): + return [] + return [r for r in (resolve_id(ref) for ref in refs) if r] + + +def _emit_id_array_pairs( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + sources = resolve_ids(config.get("sources", obj.inputs)) + outputs = resolve_ids(config.get("outputs", obj.outputs)) + n = len(sources) + len(outputs) + if len(sources) != len(outputs): + raise ValueError( + f"Program {obj.obj_id} has {len(sources)} sources and " + f"{len(outputs)} outputs" + ) + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}({n}),") + for src_id, output_id in zip(sources, outputs, strict=True): + ctx.format_helper.append_line(lines, 3, f"{src_id.upper()},") + ctx.format_helper.append_line(lines, 3, f"{output_id.upper()},") + + +def _emit_uint32( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + default = field_def.default + value = config.get(field_def.name, default if default else 0) + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}(),") + ctx.format_helper.append_line(lines, 3, f"{value},") + + +def _emit_uint32_list( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + values = config.get(field_def.name, []) + n = len(values) + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}({n}),") + for v in values: + ctx.format_helper.append_line(lines, 3, f"{int(v)},") + + +def _emit_id_array( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}(),") + field_name = field_def.name + # For standard inputs/outputs, they are in obj.inputs/obj.outputs + if field_name == "inputs": + ids = obj.inputs + elif field_name == "outputs": + ids = obj.outputs + else: + ids = obj.config.get(field_name, []) + + for obj_id in ids: + ctx.format_helper.append_line(lines, 3, f"{obj_id.upper()},") + + +def _emit_id_list( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + ids = resolve_ids(config.get(field_def.name, [])) + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({len(ids)})," + ) + for obj_id in ids: + ctx.format_helper.append_line(lines, 3, f"{obj_id.upper()},") + + +def _emit_id_single( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> None: + field_name = field_def.name + if field_name == "reset": + obj_id = obj.reset + else: + obj_id = obj.config.get(field_name) + if isinstance(obj_id, list): + obj_id = obj_id[0] if obj_id else None + obj_id = resolve_reference(obj_id) if obj_id else None + + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}(),") + if obj_id: + ctx.format_helper.append_line(lines, 3, f"{obj_id.upper()},") + else: + ctx.format_helper.append_line(lines, 3, "0,") + + +#: Generic value-types shared across program handlers -> their emitter. +_GENERIC_EMITTERS = { + "id_array_pairs": _emit_id_array_pairs, + "uint32": _emit_uint32, + "uint32_list": _emit_uint32_list, + "id_array": _emit_id_array, + "id_list": _emit_id_list, + "id_single": _emit_id_single, +} + + +def emit_generic_config_field( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit one generic config field; return whether the type was generic. + + Returns ``False`` for program-specific value-types (a handler's own + ``emit_config_field_cpp`` hook owns those). + """ + emitter = _GENERIC_EMITTERS.get(field_def.value_type) + if emitter is None: + return False + emitter(lines, field_def, obj, config, ctx) + return True + + +#: A handler's program-specific per-field hook, or ``None`` for a program type +#: with config fields but no handler module (e.g. OOT types using only the +#: generic value-types). +ConfigFieldHook = Callable[ + [ + list[str], + "ConfigField", + "ProgramObject", + dict[str, Any], + ProgFieldCppCtx, + ], + bool, +] + + +def emit_config_fields_cpp( + hook: ConfigFieldHook | None, + owner: str, + lines: list[str], + obj: ProgramObject, + config: dict[str, Any], + field_defs: list[ConfigField], + ctx: ProgFieldCppCtx, +) -> None: + """Emit every type-specific config field, hook-first then generic. + + Each field is offered to ``hook`` (a handler's ``emit_config_field_cpp``) + when present; anything it declines falls back to the shared generic + emitters. An unhandled value-type is a bug and raises rather than + silently emitting nothing. ``owner`` labels the error. + """ + for field_def in field_defs: + if hook is not None and hook(lines, field_def, obj, config, ctx): + continue + if not emit_generic_config_field(lines, field_def, obj, config, ctx): + raise ValueError( + f"{owner}: no C++ emitter for config field " + f"'{field_def.name}' (value_type '{field_def.value_type}')" + ) diff --git a/src/dawnpy/descriptor/handlers/prog_adjust.py b/src/dawnpy/descriptor/handlers/prog_adjust.py index aa3a17a..4f0cd08 100644 --- a/src/dawnpy/descriptor/handlers/prog_adjust.py +++ b/src/dawnpy/descriptor/handlers/prog_adjust.py @@ -9,20 +9,22 @@ * ``cpp_class`` binding (yaml-token ``adjust`` -> ``CProgAdjust``) * user-facing YAML config schema (``params`` field) +* C++ source emission for the ``adjust_params`` value-type * binary serializer block (cfgParams -> [offset, scale]) - -The C++ source generator path still lives in -``descriptor/prog_generators.py`` (the ``adjust_params`` value-type -emitter) - the generator carve-up is a follow-up. """ from __future__ import annotations from typing import TYPE_CHECKING, Any +from dawnpy.descriptor.config_access import config_field_is_rw from dawnpy.descriptor.definitions.type_info import ConfigField -from dawnpy.descriptor.encoding.scalar import encode_scalar_words +from dawnpy.descriptor.encoding.scalar import ( + encode_scalar_words, + format_scalar_cpp, +) from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx from dawnpy.headerdefs.bundle import header_cfg_id if TYPE_CHECKING: @@ -67,6 +69,35 @@ def emit_iobind_cpp( return True +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``adjust`` params block; return whether handled.""" + if field_def.value_type != "adjust_params": + return False + + params = config.get(field_def.name, {}) + if not isinstance(params, dict): + params = {} + + # rw is true only when a writable config IO targets these params; the + # config IO's reference emits the same cfgParams(rw) so the runtime + # cfg-id lookup matches. + rw = config_field_is_rw(ctx.rw_grants, obj.obj_id, field_def.name) + rw_arg = "true" if rw else "" + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({rw_arg})," + ) + for raw in (params.get("offset", 0), params.get("scale", 1)): + for literal in format_scalar_cpp(raw, obj.dtype): + ctx.format_helper.append_line(lines, 3, f"{literal},") + return True + + def encode_binary( items: list[tuple[int, list[int]]], obj: ProgramObject, diff --git a/src/dawnpy/descriptor/handlers/prog_bitpack.py b/src/dawnpy/descriptor/handlers/prog_bitpack.py index 3c76181..2fb56a3 100644 --- a/src/dawnpy/descriptor/handlers/prog_bitpack.py +++ b/src/dawnpy/descriptor/handlers/prog_bitpack.py @@ -4,6 +4,7 @@ from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx from dawnpy.descriptor.support.utils import resolve_reference from dawnpy.headerdefs.bundle import header_cfg_id @@ -43,6 +44,33 @@ def config_fields() -> list[ConfigField]: # pragma: no cover ] +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: Any, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``bitpack`` inputs block; return whether handled.""" + if field_def.value_type != "bitpack_inputs": + return False + + entries = config.get(field_def.name, []) + words = [] + if isinstance(entries, list): + for e in entries: + if isinstance(e, dict): + io = resolve_reference(e.get("io", "")) + words.append(io.upper() if io else "0") + words.append(str(int(e.get("bit", 0)))) + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({len(words)})," + ) + for w in words: + ctx.format_helper.append_line(lines, 3, f"{w},") + return True + + def output_shape_owned_virt_targets(obj: Any) -> set[str]: """Return the configured output-side target for ``bitpack``.""" output_ref = resolve_reference(obj.config.get("output")) diff --git a/src/dawnpy/descriptor/handlers/prog_buffer.py b/src/dawnpy/descriptor/handlers/prog_buffer.py index fb82b31..af561ce 100644 --- a/src/dawnpy/descriptor/handlers/prog_buffer.py +++ b/src/dawnpy/descriptor/handlers/prog_buffer.py @@ -16,6 +16,10 @@ from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ( + ProgFieldCppCtx, + resolve_id, +) from dawnpy.descriptor.support.utils import ( resolve_flexible_reference, resolve_reference, @@ -23,6 +27,8 @@ from dawnpy.headerdefs.bundle import header_cfg_id if TYPE_CHECKING: + from typing import Any + from dawnpy.descriptor.definitions.objects import ProgramObject from dawnpy.objectid import ObjectIdDecoder @@ -65,6 +71,43 @@ def config_fields() -> list[ConfigField]: ] +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``buffer`` iobind quads block; return whether handled.""" + if field_def.value_type != "id_array_quads": + return False + + entries = config.get(field_def.name, []) + if not isinstance(entries, list): + entries = [] + + resolved_quads: list[tuple[str, str, str, str]] = [] + for entry in entries: + if not isinstance(entry, dict): + continue + src = resolve_id(entry.get("src")) + out = resolve_id(entry.get("out")) + sel = resolve_id(entry.get("sel")) + stat = resolve_id(entry.get("stat")) + if not src or not out or not sel or not stat: + continue + resolved_quads.append((src, out, sel, stat)) + + size = 4 * len(resolved_quads) + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}({size}),") + for src, out, sel, stat in resolved_quads: + ctx.format_helper.append_line(lines, 3, f"{src.upper()},") + ctx.format_helper.append_line(lines, 3, f"{out.upper()},") + ctx.format_helper.append_line(lines, 3, f"{sel.upper()},") + ctx.format_helper.append_line(lines, 3, f"{stat.upper()},") + return True + + def output_shape_owned_virt_targets(obj: ProgramObject) -> set[str]: """Return buffer roles whose output shape is defined by the program.""" refs: set[str] = set() diff --git a/src/dawnpy/descriptor/handlers/prog_gateway.py b/src/dawnpy/descriptor/handlers/prog_gateway.py index 32c1817..0cf331c 100644 --- a/src/dawnpy/descriptor/handlers/prog_gateway.py +++ b/src/dawnpy/descriptor/handlers/prog_gateway.py @@ -16,10 +16,16 @@ from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ( + ProgFieldCppCtx, + resolve_id, +) from dawnpy.descriptor.support.utils import resolve_reference from dawnpy.headerdefs.bundle import header_cfg_id if TYPE_CHECKING: + from typing import Any + from dawnpy.descriptor.definitions.objects import ProgramObject from dawnpy.objectid import ObjectIdDecoder @@ -39,6 +45,44 @@ def config_fields() -> list[ConfigField]: ] +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``gateway`` iobind block; return whether handled.""" + if field_def.value_type != "gateway_iobind": + return False + + entries = config.get(field_def.name, []) + if not isinstance(entries, list): + entries = [] + + resolved_gateway: list[tuple[str, str, int, int]] = [] + for entry in entries: + if not isinstance(entry, dict): + continue + io1 = resolve_id(entry.get("io1")) + io2 = resolve_id(entry.get("io2")) + if not io1 or not io2: + continue + flags = int(entry.get("flags", 0)) + dim = int(entry.get("dim", 1)) + resolved_gateway.append((io1, io2, flags, dim)) + + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({4 * len(resolved_gateway)})," + ) + for io1, io2, flags, dim in resolved_gateway: + ctx.format_helper.append_line(lines, 3, f"{io1.upper()},") + ctx.format_helper.append_line(lines, 3, f"{io2.upper()},") + ctx.format_helper.append_line(lines, 3, f"{flags},") + ctx.format_helper.append_line(lines, 3, f"{dim},") + return True + + def output_shape_owned_virt_targets(obj: ProgramObject) -> set[str]: """Return gateway endpoints that may be shape-owned by the program.""" refs: set[str] = set() diff --git a/src/dawnpy/descriptor/handlers/prog_sequencer.py b/src/dawnpy/descriptor/handlers/prog_sequencer.py index 2bffc1d..1b07771 100644 --- a/src/dawnpy/descriptor/handlers/prog_sequencer.py +++ b/src/dawnpy/descriptor/handlers/prog_sequencer.py @@ -16,10 +16,13 @@ from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx from dawnpy.descriptor.support.utils import resolve_references from dawnpy.headerdefs.bundle import header_cfg_id if TYPE_CHECKING: + from typing import Any + from dawnpy.descriptor.definitions.objects import ProgramObject from dawnpy.objectid import ObjectIdDecoder @@ -99,6 +102,39 @@ def output_shape_owned_virt_targets(obj: ProgramObject) -> set[str]: return set(resolve_references(obj.config.get("targets", []))) +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: ProgramObject, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit the ``sequencer`` states block; return whether handled.""" + if field_def.value_type != "sequencer_states": + return False + + entries = config.get(field_def.name, []) + encoded: list[tuple[int, int]] = [] + + if not isinstance(entries, list): + entries = [] + + for entry in entries: + if not isinstance(entry, dict): + continue + value = int(entry.get("value", 0)) + dwell = int(entry.get("dwell_us", 0)) + encoded.append((value, dwell)) + + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({2 * len(encoded)})," + ) + for value, dwell in encoded: + ctx.format_helper.append_line(lines, 3, f"{value},") + ctx.format_helper.append_line(lines, 3, f"{dwell},") + return True + + def config_reference_cpp_line( obj: ProgramObject, field_name: str, config_loader: object ) -> str | None: diff --git a/src/dawnpy/descriptor/handlers/prog_switch.py b/src/dawnpy/descriptor/handlers/prog_switch.py index 70bf2b2..3326907 100644 --- a/src/dawnpy/descriptor/handlers/prog_switch.py +++ b/src/dawnpy/descriptor/handlers/prog_switch.py @@ -4,6 +4,7 @@ from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.words import cfg_id +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx from dawnpy.descriptor.support.utils import resolve_reference from dawnpy.headerdefs.bundle import header_cfg_id @@ -27,6 +28,45 @@ def config_fields() -> list[ConfigField]: # pragma: no cover ] +def emit_config_field_cpp( + lines: list[str], + field_def: ConfigField, + obj: Any, + config: dict[str, Any], + ctx: ProgFieldCppCtx, +) -> bool: + """Emit ``switch`` inputs/target blocks; return whether handled.""" + if field_def.value_type == "switch_inputs": + entries = config.get(field_def.name, []) + words = [] + if isinstance(entries, list): + for e in entries: + if isinstance(e, dict): + io = resolve_reference(e.get("io", "")) + words.append(io.upper() if io else "0") + words.append(str(int(e.get("match", 1)))) + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}({len(words)})," + ) + for w in words: + ctx.format_helper.append_line(lines, 3, f"{w},") + return True + + if field_def.value_type == "switch_target": + target = config.get(field_def.name, []) + tgt_ref = resolve_reference(target[0]) if target else "" + tgt_id = tgt_ref.upper() if tgt_ref else "0" + on_cmd = str(int(target[1])) if len(target) > 1 else "1" + off_cmd = str(int(target[2])) if len(target) > 2 else "0" + ctx.format_helper.append_line(lines, 2, f"{field_def.cpp_helper}(),") + ctx.format_helper.append_line(lines, 3, f"{tgt_id},") + ctx.format_helper.append_line(lines, 3, f"{on_cmd},") + ctx.format_helper.append_line(lines, 3, f"{off_cmd},") + return True + + return False + + def output_shape_owned_virt_targets(obj: Any) -> set[str]: """Return the switch target whose shape is owned by the program.""" target = obj.config.get("target", []) diff --git a/tests/descriptor/handlers/helpers.py b/tests/descriptor/handlers/helpers.py index c530425..678fe80 100644 --- a/tests/descriptor/handlers/helpers.py +++ b/tests/descriptor/handlers/helpers.py @@ -5,7 +5,15 @@ """Small object builders for handler tests.""" +from dawnpy.descriptor.config_access import ConfigRwGrants from dawnpy.descriptor.definitions.objects import IoObject +from dawnpy.descriptor.handlers._prog_config_cpp import ProgFieldCppCtx +from dawnpy.descriptor.support.formatting import DescriptorFormatHelper + + +def prog_cpp_ctx(rw_grants: ConfigRwGrants | None = None) -> ProgFieldCppCtx: + """Build a program config-field C++ emission context for tests.""" + return ProgFieldCppCtx(DescriptorFormatHelper(), rw_grants or {}) def to_io_obj(spec: dict, obj_id: str = "test_io") -> IoObject: diff --git a/tests/descriptor/handlers/test_prog_adjust.py b/tests/descriptor/handlers/test_prog_adjust.py index 04b71ec..8f04791 100644 --- a/tests/descriptor/handlers/test_prog_adjust.py +++ b/tests/descriptor/handlers/test_prog_adjust.py @@ -5,95 +5,111 @@ """Handler-owned descriptor tests.""" +import struct + import pytest from dawnpy.descriptor.definitions.objects import ProgramObject -from dawnpy.descriptor.definitions.registry import PROG_TYPES -from dawnpy.descriptor.generation.prog import ProgramConfigGenerator +from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers.prog_adjust import ( + emit_config_field_cpp, + emit_iobind_cpp, +) +from dawnpy.descriptor.support.formatting import DescriptorFormatHelper + +from .helpers import prog_cpp_ctx pytestmark = pytest.mark.usefixtures("source_free_headers") -class TestProgramConfigGenerator: - - def test_emit_adjust_params(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - obj = ProgramObject( - obj_id="adjust1", - prog_type="adjust", - instance=0, - inputs=["src0"], - outputs=["virt0"], - reset=None, - config={"params": {"offset": 3, "scale": 2}}, - ) - lines = [] - cpp_helper = "CProgAdjust::cfgParams" - config = {"params": {"offset": 3, "scale": 2}} - prog_gen._emit_adjust_params(lines, cpp_helper, "params", obj, config) - assert lines == [ - " CProgAdjust::cfgParams(),", - " 3,", - " 2,", - ] - - def test_emit_adjust_params_float(self): - """A float-dtype prog encodes offset/scale as IEEE-754 bit patterns.""" - import struct - - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - obj = ProgramObject( - obj_id="adjust1", - prog_type="adjust", - instance=0, - inputs=["src0"], - outputs=["virt0"], - reset=None, - config={"params": {"offset": -8.7, "scale": 1.0}}, - dtype="float", - ) - lines = [] - cpp_helper = "CProgAdjust::cfgParams" - config = {"params": {"offset": -8.7, "scale": 1.0}} - prog_gen._emit_adjust_params(lines, cpp_helper, "params", obj, config) - - off = int.from_bytes(struct.pack(" ProgramObject: + base = dict( + obj_id="adjust1", + prog_type="adjust", + instance=0, + inputs=["src0"], + outputs=["virt0"], + reset=None, + config={"params": {"offset": 3, "scale": 2}}, + ) + base.update(kw) + return ProgramObject(**base) + + +def _params_field() -> ConfigField: + return ConfigField( + name="params", + cpp_helper="CProgAdjust::cfgParams", + value_type="adjust_params", + ) + + +def test_emit_adjust_params(): + obj = _obj(config={"params": {"offset": 3, "scale": 2}}) + lines: list[str] = [] + handled = emit_config_field_cpp( + lines, _params_field(), obj, obj.config, prog_cpp_ctx() + ) + assert handled is True + assert lines == [ + " CProgAdjust::cfgParams(),", + " 3,", + " 2,", + ] + + +def test_emit_adjust_params_float(): + """A float-dtype prog encodes offset/scale as IEEE-754 bit patterns.""" + obj = _obj( + config={"params": {"offset": -8.7, "scale": 1.0}}, dtype="float" + ) + lines: list[str] = [] + emit_config_field_cpp( + lines, _params_field(), obj, obj.config, prog_cpp_ctx() + ) + + off = int.from_bytes(struct.pack(" ConfigField: + return ConfigField( + name="iobind", + cpp_helper="CProgBuffer::cfgIdIOBind", + value_type="id_array_quads", + ) + + +def test_emit_id_array_quads(): + config = {"iobind": [{"src": "a", "out": "b", "sel": "c", "stat": "d"}]} + lines: list[str] = [] + assert emit_config_field_cpp(lines, _field(), None, config, prog_cpp_ctx()) + assert lines == [ + " CProgBuffer::cfgIdIOBind(4),", + " A,", + " B,", + " C,", + " D,", + ] + + +def test_emit_id_array_quads_skips_invalid_entries(): + config = { + "iobind": [ + "not_a_dict", + {"src": "a", "out": "b"}, # missing sel/stat + {"src": "a", "out": "b", "sel": "c", "stat": None}, # invalid + ] + } + lines: list[str] = [] + emit_config_field_cpp(lines, _field(), None, config, prog_cpp_ctx()) + assert lines == [" CProgBuffer::cfgIdIOBind(0),"] + + +def test_emit_id_array_quads_non_list(): + lines: list[str] = [] + emit_config_field_cpp( + lines, _field(), None, {"iobind": "notalist"}, prog_cpp_ctx() + ) + assert lines == [" CProgBuffer::cfgIdIOBind(0),"] + + +def test_emit_declines_other_value_type(): + field = ConfigField(name="depth", cpp_helper="H", value_type="uint32") + lines: list[str] = [] + handled = emit_config_field_cpp(lines, field, None, {}, prog_cpp_ctx()) + assert handled is False + assert lines == [] diff --git a/tests/descriptor/handlers/test_prog_common.py b/tests/descriptor/handlers/test_prog_common.py index 46fc8e6..77e0ddc 100644 --- a/tests/descriptor/handlers/test_prog_common.py +++ b/tests/descriptor/handlers/test_prog_common.py @@ -3,316 +3,22 @@ # SPDX-License-Identifier: Apache-2.0 # -"""Handler-owned descriptor tests.""" +"""Program config generator integration tests. + +Per-field C++ emitters are covered by ``test_prog_config_cpp.py`` (shared +generic emitters) and each handler's own test module (program-specific +value-types). This module exercises the ``generate_prog_config`` orchestrator. +""" import pytest from dawnpy.descriptor.definitions.objects import ProgramObject -from dawnpy.descriptor.definitions.registry import PROG_TYPES from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.generation.generator import DescriptorGenerator -from dawnpy.descriptor.generation.prog import ProgramConfigGenerator pytestmark = pytest.mark.usefixtures("source_free_headers") -class TestProgramConfigGenerator: - - def test_emit_id_array_pairs(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgStatsMin::cfgIdIOBind" - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=["io1"], - outputs=["io2"], - reset=None, - config={}, - ) - config = {"sources": ["io1"], "outputs": ["io2"]} - prog_gen._emit_id_array_pairs(lines, cpp_helper, obj, config) - assert lines == [ - " CProgStatsMin::cfgIdIOBind(2),", - " IO1,", - " IO2,", - ] - - def test_emit_id_array_pairs_interleaves_multiple_binds(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgStatsMin::cfgIdIOBind" - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=["io1", "io2"], - outputs=["out1", "out2"], - reset=None, - config={}, - ) - config = {"sources": ["io1", "io2"], "outputs": ["out1", "out2"]} - prog_gen._emit_id_array_pairs(lines, cpp_helper, obj, config) - assert lines == [ - " CProgStatsMin::cfgIdIOBind(4),", - " IO1,", - " OUT1,", - " IO2,", - " OUT2,", - ] - - def test_emit_uint32(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgSampling::cfgIdIOInterval" - config = {"interval": 50000} - prog_gen._emit_uint32(lines, cpp_helper, "interval", config) - assert lines == [ - " CProgSampling::cfgIdIOInterval(),", - " 50000,", - ] - - def test_emit_uint32_uses_field_default(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - prog_gen._emit_uint32( - lines, "CProgBuffer::cfgIdChunkSize", "chunk_size", {}, "1" - ) - assert lines == [ - " CProgBuffer::cfgIdChunkSize(),", - " 1,", - ] - - def test_emit_id_array(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgCommon::cfgIdInput" - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=["io1", "io2"], - outputs=[], - reset=None, - config={}, - ) - prog_gen._emit_id_array(lines, cpp_helper, "inputs", obj) - assert lines == [ - " CProgCommon::cfgIdInput(),", - " IO1,", - " IO2,", - ] - - def test_emit_id_single(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - # Case with ID - lines = [] - cpp_helper = "CProgCommon::cfgIdReset" - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=[], - outputs=[], - reset="io1", - config={}, - ) - prog_gen._emit_id_single(lines, cpp_helper, "reset", obj) - assert lines == [" CProgCommon::cfgIdReset(),", " IO1,"] - - # Case without ID - lines = [] - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=[], - outputs=[], - reset=None, - config={}, - ) - prog_gen._emit_id_single(lines, cpp_helper, "reset", obj) - assert lines == [" CProgCommon::cfgIdReset(),", " 0,"] - - def test_emit_id_array_quads(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - # Successful case - lines = [] - cpp_helper = "CProgBuffer::cfgIdIOBind" - config = { - "iobind": [ - {"src": "io1", "out": "io2", "sel": "io3", "stat": "io4"} - ] - } - prog_gen._emit_id_array_quads(lines, cpp_helper, "iobind", config) - assert " CProgBuffer::cfgIdIOBind(4)," in lines[0] - - # Negative cases for coverage - lines = [] - config = { - "iobind": [ - "not_a_dict", - {"src": "io1", "out": "io2"}, # missing sel/stat - { - "src": "io1", - "out": "io2", - "sel": "io3", - "stat": None, - }, # invalid - ] - } - prog_gen._emit_id_array_quads(lines, cpp_helper, "iobind", config) - assert " CProgBuffer::cfgIdIOBind(0)," in lines[0] - - def test_emit_id_list(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgSequencer::cfgIdTargets" - config = {"targets": ["io1", {"id": "io2"}]} - prog_gen._emit_id_list(lines, cpp_helper, "targets", config) - assert lines == [ - " CProgSequencer::cfgIdTargets(2),", - " IO1,", - " IO2,", - ] - - def test_emit_type_field_coverage(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - # Test id_single coverage - lines = [] - field_def = ConfigField( - name="reset", - value_type="id_single", - cpp_helper="H", - ) - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=[], - outputs=[], - reset="io1", - config={}, - ) - prog_gen._emit_type_field(lines, field_def, obj, {}) - assert "H()," in lines[0] - assert " IO1," in lines[1] - - # Test id_array coverage - lines = [] - field_def = ConfigField( - name="inputs", - value_type="id_array", - cpp_helper="H", - ) - obj = ProgramObject( - obj_id="p1", - prog_type="stats", - instance=0, - inputs=["io1"], - outputs=[], - reset=None, - config={}, - ) - prog_gen._emit_type_field(lines, field_def, obj, {}) - assert "H()," in lines[0] - assert " IO1," in lines[1] - - # Test gateway_iobind coverage - lines = [] - field_def = ConfigField( - name="iobind", - value_type="gateway_iobind", - cpp_helper="H", - ) - config = {"iobind": [{"io1": "a", "io2": "b"}]} - prog_gen._emit_type_field(lines, field_def, obj, config) - assert "H(4)," in lines[0] - - # Test id_array_quads coverage - lines = [] - field_def = ConfigField( - name="iobind", - value_type="id_array_quads", - cpp_helper="H", - ) - config = { - "iobind": [{"src": "a", "out": "b", "sel": "c", "stat": "d"}] - } - prog_gen._emit_type_field(lines, field_def, {}, config) - assert "H(4)," in lines[0] - - # Test adjust_params coverage - lines = [] - field_def = ConfigField( - name="params", - value_type="adjust_params", - cpp_helper="H", - ) - config = {"params": {"offset": 7, "scale": 5}} - prog_gen._emit_type_field(lines, field_def, obj, config) - assert lines == [" H(),", " 7,", " 5,"] - - # Test id_list coverage - lines = [] - field_def = ConfigField( - name="targets", - value_type="id_list", - cpp_helper="H", - ) - config = {"targets": ["a", {"id": "b"}]} - prog_gen._emit_type_field(lines, field_def, obj, config) - assert lines == [" H(2),", " A,", " B,"] - - # Test sequencer_states coverage - lines = [] - field_def = ConfigField( - name="states", - value_type="sequencer_states", - cpp_helper="H", - ) - config = {"states": [{"value": 10, "dwell_us": 20}]} - prog_gen._emit_type_field(lines, field_def, obj, config) - assert lines == [" H(2),", " 10,", " 20,"] - - def test_generate_prog_config_handles_id_single(monkeypatch): generator = DescriptorGenerator() monkeypatch.setattr( @@ -326,7 +32,6 @@ def test_generate_prog_config_handles_id_single(monkeypatch): ), ], ) - from dawnpy.descriptor.definitions.objects import ProgramObject obj = ProgramObject.from_spec( { diff --git a/tests/descriptor/handlers/test_prog_config_cpp.py b/tests/descriptor/handlers/test_prog_config_cpp.py new file mode 100644 index 0000000..61bc11b --- /dev/null +++ b/tests/descriptor/handlers/test_prog_config_cpp.py @@ -0,0 +1,337 @@ +# tools/dawnpy/tests/descriptor/handlers/test_prog_config_cpp.py +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""Tests for the shared program config-field C++ emitters.""" + +import pytest + +from dawnpy.descriptor.definitions.objects import ProgramObject +from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers._prog_config_cpp import ( + emit_config_fields_cpp, + emit_generic_config_field, +) + +from .helpers import prog_cpp_ctx + +pytestmark = pytest.mark.usefixtures("source_free_headers") + + +def _obj(**kw) -> ProgramObject: + base = dict( + obj_id="p1", + prog_type="stats", + instance=0, + inputs=[], + outputs=[], + reset=None, + config={}, + ) + base.update(kw) + return ProgramObject(**base) + + +def _field(value_type: str, name: str = "f", cpp_helper: str = "H", **kw): + return ConfigField( + name=name, cpp_helper=cpp_helper, value_type=value_type, **kw + ) + + +class TestGenericEmitters: + + def test_id_array_pairs(self): + obj = _obj(inputs=["io1"], outputs=["io2"]) + lines: list[str] = [] + handled = emit_generic_config_field( + lines, + _field("id_array_pairs", cpp_helper="CProgStatsMin::cfgIdIOBind"), + obj, + {"sources": ["io1"], "outputs": ["io2"]}, + prog_cpp_ctx(), + ) + assert handled is True + assert lines == [ + " CProgStatsMin::cfgIdIOBind(2),", + " IO1,", + " IO2,", + ] + + def test_id_array_pairs_interleaves_multiple_binds(self): + obj = _obj(inputs=["io1", "io2"], outputs=["out1", "out2"]) + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_array_pairs", cpp_helper="CProgStatsMin::cfgIdIOBind"), + obj, + {"sources": ["io1", "io2"], "outputs": ["out1", "out2"]}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgStatsMin::cfgIdIOBind(4),", + " IO1,", + " OUT1,", + " IO2,", + " OUT2,", + ] + + def test_id_array_pairs_mismatch_raises(self): + obj = _obj(inputs=["io1"], outputs=["io2"]) + with pytest.raises(ValueError, match="sources"): + emit_generic_config_field( + [], + _field("id_array_pairs", cpp_helper="H"), + obj, + {"sources": ["io1", "io2"], "outputs": ["io2"]}, + prog_cpp_ctx(), + ) + + def test_uint32(self): + lines: list[str] = [] + emit_generic_config_field( + lines, + _field( + "uint32", + name="interval", + cpp_helper="CProgSampling::cfgIdIOInterval", + ), + _obj(), + {"interval": 50000}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgSampling::cfgIdIOInterval(),", + " 50000,", + ] + + def test_uint32_uses_field_default(self): + lines: list[str] = [] + emit_generic_config_field( + lines, + _field( + "uint32", + name="chunk_size", + cpp_helper="CProgBuffer::cfgIdChunkSize", + default="1", + ), + _obj(), + {}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgBuffer::cfgIdChunkSize(),", + " 1,", + ] + + def test_uint32_list(self): + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("uint32_list", name="vals", cpp_helper="H"), + _obj(), + {"vals": [1, 2, 3]}, + prog_cpp_ctx(), + ) + assert lines == [" H(3),", " 1,", " 2,", " 3,"] + + def test_id_array(self): + obj = _obj(inputs=["io1", "io2"]) + lines: list[str] = [] + emit_generic_config_field( + lines, + _field( + "id_array", name="inputs", cpp_helper="CProgCommon::cfgIdInput" + ), + obj, + {}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgCommon::cfgIdInput(),", + " IO1,", + " IO2,", + ] + + def test_id_single_with_and_without_id(self): + helper = "CProgCommon::cfgIdReset" + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_single", name="reset", cpp_helper=helper), + _obj(reset="io1"), + {}, + prog_cpp_ctx(), + ) + assert lines == [" CProgCommon::cfgIdReset(),", " IO1,"] + + lines = [] + emit_generic_config_field( + lines, + _field("id_single", name="reset", cpp_helper=helper), + _obj(reset=None), + {}, + prog_cpp_ctx(), + ) + assert lines == [" CProgCommon::cfgIdReset(),", " 0,"] + + def test_id_list(self): + lines: list[str] = [] + emit_generic_config_field( + lines, + _field( + "id_list", + name="targets", + cpp_helper="CProgSequencer::cfgIdTargets", + ), + _obj(), + {"targets": ["io1", {"id": "io2"}]}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgSequencer::cfgIdTargets(2),", + " IO1,", + " IO2,", + ] + + def test_id_array_outputs(self): + obj = _obj(outputs=["o1", "o2"]) + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_array", name="outputs", cpp_helper="H"), + obj, + {}, + prog_cpp_ctx(), + ) + assert lines == [" H(),", " O1,", " O2,"] + + def test_id_array_custom_field_from_obj_config(self): + obj = _obj(config={"extra": ["x1"]}) + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_array", name="extra", cpp_helper="H"), + obj, + {}, + prog_cpp_ctx(), + ) + assert lines == [" H(),", " X1,"] + + def test_id_single_from_config_list(self): + obj = _obj(config={"sel": ["a", "b"]}) + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_single", name="sel", cpp_helper="H"), + obj, + {}, + prog_cpp_ctx(), + ) + assert lines == [" H(),", " A,"] + + def test_id_list_ignores_non_list_value(self): + lines: list[str] = [] + emit_generic_config_field( + lines, + _field("id_list", name="targets", cpp_helper="H"), + _obj(), + {"targets": "notalist"}, + prog_cpp_ctx(), + ) + assert lines == [" H(0),"] + + def test_returns_false_for_program_specific_type(self): + lines: list[str] = [] + handled = emit_generic_config_field( + lines, + _field("fusion_params", name="params"), + _obj(), + {"params": {}}, + prog_cpp_ctx(), + ) + assert handled is False + assert lines == [] + + +class TestEmitConfigFieldsDispatch: + + def test_no_hook_uses_generic(self): + """OOT program with fields but no handler uses the generic path.""" + lines: list[str] = [] + emit_config_fields_cpp( + None, + "oot_prog", + lines, + _obj(reset="io1"), + {}, + [_field("id_single", name="reset", cpp_helper="H")], + prog_cpp_ctx(), + ) + assert lines == [" H(),", " IO1,"] + + def test_hook_takes_precedence_over_generic(self): + calls: list[str] = [] + + def hook(lines, field_def, obj, config, ctx): + calls.append(field_def.name) + lines.append("hooked") + return True + + lines: list[str] = [] + emit_config_fields_cpp( + hook, + "owner", + lines, + _obj(), + {}, + [_field("uint32", name="depth", cpp_helper="H")], + prog_cpp_ctx(), + ) + assert calls == ["depth"] + assert lines == ["hooked"] + + def test_hook_declines_falls_back_to_generic(self): + def hook(lines, field_def, obj, config, ctx): + return False + + lines: list[str] = [] + emit_config_fields_cpp( + hook, + "owner", + lines, + _obj(), + {"depth": 4}, + [_field("uint32", name="depth", cpp_helper="H")], + prog_cpp_ctx(), + ) + assert lines == [" H(),", " 4,"] + + def test_unknown_value_type_raises(self): + with pytest.raises(ValueError, match="no .* emitter"): + emit_config_fields_cpp( + None, + "owner", + [], + _obj(), + {}, + [_field("bogus_type", name="x", cpp_helper="H")], + prog_cpp_ctx(), + ) + + def test_unclaimed_custom_type_raises(self): + """A hook that declines a program-specific type is a bug -> raise.""" + + def hook(lines, field_def, obj, config, ctx): + return False + + with pytest.raises(ValueError, match="fusion_params"): + emit_config_fields_cpp( + hook, + "fusion", + [], + _obj(), + {"params": {}}, + [_field("fusion_params", name="params", cpp_helper="H")], + prog_cpp_ctx(), + ) diff --git a/tests/descriptor/handlers/test_prog_fusion.py b/tests/descriptor/handlers/test_prog_fusion.py index 35938b3..dad2996 100644 --- a/tests/descriptor/handlers/test_prog_fusion.py +++ b/tests/descriptor/handlers/test_prog_fusion.py @@ -10,19 +10,19 @@ import pytest from dawnpy.descriptor.definitions.objects import ProgramObject -from dawnpy.descriptor.definitions.registry import PROG_TYPES -from dawnpy.descriptor.generation.prog import ProgramConfigGenerator +from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.handlers import PROG_HANDLER_REGISTRY from dawnpy.descriptor.handlers.prog_fusion import ( PARAM_DEFAULTS, PARAM_ORDER, + emit_config_field_cpp, encode_binary, output_shape_owned_virt_targets, validate_object, validate_object_refs, ) -from .helpers import to_io_obj +from .helpers import prog_cpp_ctx, to_io_obj pytestmark = pytest.mark.usefixtures("source_free_headers") @@ -183,17 +183,18 @@ def test_mag_is_optional(self): assert validate_object(_obj()) == [] def test_emit_fusion_params(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES + field = ConfigField( + name="params", + cpp_helper="CProgFusion::cfgParams", + value_type="fusion_params", ) - lines = [] + lines: list[str] = [] - prog_gen._emit_fusion_params( - lines, "CProgFusion::cfgParams", "params", _obj(), _obj().config + handled = emit_config_field_cpp( + lines, field, _obj(), _obj().config, prog_cpp_ctx() ) + assert handled is True assert lines == [ " CProgFusion::cfgParams(),", f" {_f32(0.5):#010x},", @@ -201,3 +202,40 @@ def test_emit_fusion_params(self): f" {_f32(5.0):#010x},", f" {_f32(50.0):#010x},", ] + + def test_emit_fusion_params_rw_when_granted(self): + field = ConfigField( + name="params", + cpp_helper="CProgFusion::cfgParams", + value_type="fusion_params", + ) + lines: list[str] = [] + ctx = prog_cpp_ctx({("fusion1", "params"): True}) + + emit_config_field_cpp(lines, field, _obj(), _obj().config, ctx) + + assert lines[0] == " CProgFusion::cfgParams(true)," + + def test_emit_fusion_params_non_dict_uses_defaults(self): + field = ConfigField( + name="params", + cpp_helper="CProgFusion::cfgParams", + value_type="fusion_params", + ) + lines: list[str] = [] + emit_config_field_cpp( + lines, field, _obj(), {"params": "nonsense"}, prog_cpp_ctx() + ) + assert lines == [ + " CProgFusion::cfgParams(),", + *[f" {_f32(PARAM_DEFAULTS[n]):#010x}," for n in PARAM_ORDER], + ] + + def test_emit_declines_other_value_type(self): + field = ConfigField(name="x", cpp_helper="H", value_type="uint32") + lines: list[str] = [] + handled = emit_config_field_cpp( + lines, field, _obj(), {}, prog_cpp_ctx() + ) + assert handled is False + assert lines == [] diff --git a/tests/descriptor/handlers/test_prog_gateway.py b/tests/descriptor/handlers/test_prog_gateway.py index 9fc8bef..0d87fa9 100644 --- a/tests/descriptor/handlers/test_prog_gateway.py +++ b/tests/descriptor/handlers/test_prog_gateway.py @@ -7,37 +7,59 @@ import pytest -from dawnpy.descriptor.definitions.registry import PROG_TYPES -from dawnpy.descriptor.generation.prog import ProgramConfigGenerator +from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers.prog_gateway import emit_config_field_cpp + +from .helpers import prog_cpp_ctx pytestmark = pytest.mark.usefixtures("source_free_headers") -class TestProgramConfigGenerator: - - def test_emit_gateway_iobind(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - # Successful case - lines = [] - cpp_helper = "CProgGateway::cfgIdIOBind" - config = { - "iobind": [{"io1": "io1", "io2": "io2", "flags": 1, "dim": 2}] - } - prog_gen._emit_gateway_iobind(lines, cpp_helper, "iobind", config) - assert " CProgGateway::cfgIdIOBind(4)," in lines[0] - - # Negative cases for coverage - lines = [] - config = { - "iobind": [ - "not_a_dict", - {"io1": "io1"}, # missing io2 - {"io1": "io1", "io2": None}, # invalid io2 - ] - } - prog_gen._emit_gateway_iobind(lines, cpp_helper, "iobind", config) - assert " CProgGateway::cfgIdIOBind(0)," in lines[0] +def _field(): + return ConfigField( + name="iobind", + cpp_helper="CProgGateway::cfgIdIOBind", + value_type="gateway_iobind", + ) + + +def test_emit_gateway_iobind(): + config = {"iobind": [{"io1": "io1", "io2": "io2", "flags": 1, "dim": 2}]} + lines: list[str] = [] + assert emit_config_field_cpp(lines, _field(), None, config, prog_cpp_ctx()) + assert lines == [ + " CProgGateway::cfgIdIOBind(4),", + " IO1,", + " IO2,", + " 1,", + " 2,", + ] + + +def test_emit_gateway_iobind_skips_invalid_entries(): + config = { + "iobind": [ + "not_a_dict", + {"io1": "io1"}, # missing io2 + {"io1": "io1", "io2": None}, # invalid io2 + ] + } + lines: list[str] = [] + emit_config_field_cpp(lines, _field(), None, config, prog_cpp_ctx()) + assert " CProgGateway::cfgIdIOBind(0)," in lines[0] + + +def test_emit_gateway_iobind_non_list(): + lines: list[str] = [] + emit_config_field_cpp( + lines, _field(), None, {"iobind": "notalist"}, prog_cpp_ctx() + ) + assert lines == [" CProgGateway::cfgIdIOBind(0),"] + + +def test_emit_declines_other_value_type(): + field = ConfigField(name="x", cpp_helper="H", value_type="uint32") + lines: list[str] = [] + handled = emit_config_field_cpp(lines, field, None, {}, prog_cpp_ctx()) + assert handled is False + assert lines == [] diff --git a/tests/descriptor/handlers/test_prog_sequencer.py b/tests/descriptor/handlers/test_prog_sequencer.py index 44b7b27..0b3ab98 100644 --- a/tests/descriptor/handlers/test_prog_sequencer.py +++ b/tests/descriptor/handlers/test_prog_sequencer.py @@ -7,33 +7,74 @@ import pytest -from dawnpy.descriptor.definitions.registry import PROG_TYPES -from dawnpy.descriptor.generation.prog import ProgramConfigGenerator +from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers.prog_sequencer import emit_config_field_cpp + +from .helpers import prog_cpp_ctx pytestmark = pytest.mark.usefixtures("source_free_headers") -class TestProgramConfigGenerator: - - def test_emit_sequencer_states(self): - from dawnpy.descriptor.definitions.loader import ConfigLoader - - prog_gen = ProgramConfigGenerator( - config_loader=ConfigLoader(), prog_types=PROG_TYPES - ) - lines = [] - cpp_helper = "CProgSequencer::cfgIdStates" - config = { - "states": [ - {"value": 1, "dwell_us": 500000}, - {"value": "2", "dwell_us": "600000"}, - ] - } - prog_gen._emit_sequencer_states(lines, cpp_helper, "states", config) - assert lines == [ - " CProgSequencer::cfgIdStates(4),", - " 1,", - " 500000,", - " 2,", - " 600000,", +def test_emit_sequencer_states(): + field = ConfigField( + name="states", + cpp_helper="CProgSequencer::cfgIdStates", + value_type="sequencer_states", + ) + config = { + "states": [ + {"value": 1, "dwell_us": 500000}, + {"value": "2", "dwell_us": "600000"}, ] + } + lines: list[str] = [] + assert emit_config_field_cpp(lines, field, None, config, prog_cpp_ctx()) + assert lines == [ + " CProgSequencer::cfgIdStates(4),", + " 1,", + " 500000,", + " 2,", + " 600000,", + ] + + +def test_emit_sequencer_states_non_list(): + field = ConfigField( + name="states", + cpp_helper="CProgSequencer::cfgIdStates", + value_type="sequencer_states", + ) + lines: list[str] = [] + emit_config_field_cpp( + lines, field, None, {"states": "notalist"}, prog_cpp_ctx() + ) + assert lines == [" CProgSequencer::cfgIdStates(0),"] + + +def test_emit_sequencer_states_skips_non_dict_entries(): + field = ConfigField( + name="states", + cpp_helper="CProgSequencer::cfgIdStates", + value_type="sequencer_states", + ) + lines: list[str] = [] + emit_config_field_cpp( + lines, + field, + None, + {"states": ["notadict", {"value": 1, "dwell_us": 2}]}, + prog_cpp_ctx(), + ) + assert lines == [ + " CProgSequencer::cfgIdStates(2),", + " 1,", + " 2,", + ] + + +def test_emit_declines_other_value_type(): + field = ConfigField(name="x", cpp_helper="H", value_type="uint32") + lines: list[str] = [] + handled = emit_config_field_cpp(lines, field, None, {}, prog_cpp_ctx()) + assert handled is False + assert lines == [] diff --git a/tests/descriptor/handlers/test_prog_switch.py b/tests/descriptor/handlers/test_prog_switch.py new file mode 100644 index 0000000..4910906 --- /dev/null +++ b/tests/descriptor/handlers/test_prog_switch.py @@ -0,0 +1,75 @@ +# tools/dawnpy/tests/descriptor/handlers/test_prog_switch.py +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""C++ emission tests for the ``switch`` PROG handler.""" + +import pytest + +from dawnpy.descriptor.definitions.type_info import ConfigField +from dawnpy.descriptor.handlers.prog_switch import emit_config_field_cpp + +from .helpers import prog_cpp_ctx + +pytestmark = pytest.mark.usefixtures("source_free_headers") + + +def test_emit_switch_inputs(): + field = ConfigField( + name="inputs", + cpp_helper="CProgSwitch::cfgIdInputs", + value_type="switch_inputs", + ) + config = {"inputs": [{"io": "io1", "match": 2}, {"io": "io2"}]} + lines: list[str] = [] + assert emit_config_field_cpp(lines, field, None, config, prog_cpp_ctx()) + assert lines == [ + " CProgSwitch::cfgIdInputs(4),", + " IO1,", + " 2,", + " IO2,", + " 1,", + ] + + +def test_emit_switch_target(): + field = ConfigField( + name="target", + cpp_helper="CProgSwitch::cfgIdTarget", + value_type="switch_target", + ) + lines: list[str] = [] + emit_config_field_cpp( + lines, field, None, {"target": ["tgt1", 5, 6]}, prog_cpp_ctx() + ) + assert lines == [ + " CProgSwitch::cfgIdTarget(),", + " TGT1,", + " 5,", + " 6,", + ] + + +def test_emit_switch_target_defaults(): + field = ConfigField( + name="target", + cpp_helper="CProgSwitch::cfgIdTarget", + value_type="switch_target", + ) + lines: list[str] = [] + emit_config_field_cpp(lines, field, None, {"target": []}, prog_cpp_ctx()) + assert lines == [ + " CProgSwitch::cfgIdTarget(),", + " 0,", + " 1,", + " 0,", + ] + + +def test_emit_declines_other_value_type(): + field = ConfigField(name="x", cpp_helper="H", value_type="uint32") + lines: list[str] = [] + handled = emit_config_field_cpp(lines, field, None, {}, prog_cpp_ctx()) + assert handled is False + assert lines == [] From 361dd16877a1d107d817ed80fe46171903ce90e4 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Fri, 10 Jul 2026 17:04:57 +0200 Subject: [PATCH 3/3] descriptor: unify per-handler config emission across io/prog/proto Give every descriptor family the same two-tier ownership mechanism for C++ config emission, symmetric with the binary path: 1. generic shared field loop (per-family value-type vocabulary) 2. per-field emit_config_field_cpp hook for type-specific value-types 3. whole-object generate_cpp escape hatch for complex handlers Signed-off-by: raiden00pl --- .../descriptor/generation/io_codegen.py | 19 ++++++ src/dawnpy/descriptor/generation/prog.py | 17 ++++- src/dawnpy/descriptor/generation/prog_base.py | 31 +++++++++ .../descriptor/generation/proto_generic.py | 65 +++++++------------ src/dawnpy/descriptor/handlers/_base.py | 4 ++ .../handlers/_proto_nxscope_common.py | 41 ++++++++++++ .../handlers/proto_nxscope_dummy.py | 2 + .../handlers/proto_nxscope_serial.py | 2 + .../descriptor/handlers/proto_nxscope_udp.py | 2 + tests/descriptor/handlers/test_io_common.py | 57 ++++++++++++++++ tests/descriptor/handlers/test_prog_common.py | 19 ++++++ .../descriptor/handlers/test_proto_generic.py | 42 ++++++++++-- 12 files changed, 251 insertions(+), 50 deletions(-) create mode 100644 src/dawnpy/descriptor/generation/prog_base.py diff --git a/src/dawnpy/descriptor/generation/io_codegen.py b/src/dawnpy/descriptor/generation/io_codegen.py index ff482bc..3473c6f 100644 --- a/src/dawnpy/descriptor/generation/io_codegen.py +++ b/src/dawnpy/descriptor/generation/io_codegen.py @@ -205,6 +205,21 @@ def generate_io_config( # noqa: C901 else [] ) + # Per-field C++ hook: an IO handler owns emission of its own + # type-specific value-types (symmetric with the binary path); + # anything it declines falls back to the generic loop below. + field_hook = getattr(handler, "emit_config_field_cpp", None) + hook_ctx = ( + IoGeneratorContext( + config_loader=self._config_loader, + format_helper=self._format_helper, + objects=self._objects(), + config_rw_grants=self._config_rw_grants(), + ) + if field_hook is not None + else None + ) + # Count how many config items are present. ``limits`` expands # into three cfg items (min/max/step), every other field is one. config_count = 0 @@ -224,6 +239,10 @@ def generate_io_config( # noqa: C901 for field_def in field_defs: field_name = field_def.name if field_name in config: + if field_hook is not None and field_hook( + lines, field_def, obj, hook_ctx + ): + continue cpp_helper = field_def.cpp_helper value = config[field_name] value_type = field_def.value_type or "auto" diff --git a/src/dawnpy/descriptor/generation/prog.py b/src/dawnpy/descriptor/generation/prog.py index 29c206f..72f95fd 100644 --- a/src/dawnpy/descriptor/generation/prog.py +++ b/src/dawnpy/descriptor/generation/prog.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any from dawnpy.descriptor.config_access import ConfigRwGrants +from dawnpy.descriptor.generation.prog_base import ProgGeneratorContext from dawnpy.descriptor.handlers import PROG_HANDLER_REGISTRY from dawnpy.descriptor.handlers._prog_config_cpp import ( ProgFieldCppCtx, @@ -59,6 +60,21 @@ def generate_prog_config( # pragma: no cover prog_type = obj.prog_type config = obj.config + handler = PROG_HANDLER_REGISTRY.get(prog_type) + + # Per-type C++ emitter takes priority: a handler that owns its whole + # config block exposes generate_cpp(macro_name, obj, ctx) - symmetric + # with the IO/PROTO escape hatch. Handlers with only type-specific + # fields use the emit_config_field_cpp hook below instead. + if handler is not None and hasattr(handler, "generate_cpp"): + gctx = ProgGeneratorContext( + config_loader=self._config_loader, + prog_types=self._prog_types, + format_helper=self._format_helper, + config_rw_grants=self._config_rw_grants(), + ) + return list(handler.generate_cpp(macro_name, obj, gctx)) + # Get program class info prog_info = self._prog_types[prog_type] cpp_class = prog_info.cpp_class @@ -81,7 +97,6 @@ def generate_prog_config( # pragma: no cover lines, 1, f"{macro_name}, {cfg_count}," ) - handler = PROG_HANDLER_REGISTRY.get(prog_type) iobind_handled = False if handler is not None and total_ids > 0: iobind_handled = handler.emit_iobind_cpp( diff --git a/src/dawnpy/descriptor/generation/prog_base.py b/src/dawnpy/descriptor/generation/prog_base.py new file mode 100644 index 0000000..ba0baf4 --- /dev/null +++ b/src/dawnpy/descriptor/generation/prog_base.py @@ -0,0 +1,31 @@ +# tools/dawnpy/src/dawnpy/descriptor/generation/prog_base.py +# +# SPDX-License-Identifier: Apache-2.0 +# + +"""Shared dependency context for the program C++ source generator. + +Mirrors :mod:`dawnpy.descriptor.generation.io_runtime` and +:mod:`dawnpy.descriptor.generation.proto_base`: holds the dispatch context +passed to a program handler's optional ``generate_cpp(macro_name, obj, ctx)`` +whole-object override. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from dawnpy.descriptor.config_access import ConfigRwGrants + from dawnpy.descriptor.support.formatting import DescriptorFormatHelper + + +@dataclass +class ProgGeneratorContext: + """Shared dependencies passed to a per-program C++ generator call.""" + + config_loader: Any + prog_types: dict[str, Any] + format_helper: DescriptorFormatHelper + config_rw_grants: ConfigRwGrants = field(default_factory=dict) diff --git a/src/dawnpy/descriptor/generation/proto_generic.py b/src/dawnpy/descriptor/generation/proto_generic.py index d674f8d..d2760c0 100644 --- a/src/dawnpy/descriptor/generation/proto_generic.py +++ b/src/dawnpy/descriptor/generation/proto_generic.py @@ -7,16 +7,21 @@ from __future__ import annotations +from collections.abc import Callable from typing import TYPE_CHECKING, Any from dawnpy.descriptor.definitions.type_info import ConfigField -from dawnpy.descriptor.support.utils import resolve_flexible_reference +from dawnpy.descriptor.handlers import PROTO_HANDLER_REGISTRY if TYPE_CHECKING: from dawnpy.descriptor.definitions.objects import ProtocolObject from .proto_base import ProtoGeneratorContext +#: A protocol handler's per-field C++ hook: appends lines for its own +#: protocol-specific value-types, returns whether it handled the field. +ProtoFieldHook = Callable[[list[str], ConfigField, Any, Any], bool] + class GenericProtoConfigGenerator: """Generate custom protocol fields from generic schema definitions. @@ -71,43 +76,18 @@ def pack_string_array_field( return len(values), all_words return len(all_words), all_words - def generate_nxscope_iobind2_field( - self, value: Any, field: ConfigField, cpp_helper: str - ) -> list[str]: - """Generate nxscope iobind2 config field.""" - lines: list[str] = [] - entries = value if isinstance(value, list) else [] - resolved_entries = [] - for entry in entries: - if isinstance(entry, str): - name = "" - elif isinstance(entry, dict): - name = entry.get("name", "") - else: - continue - resolved_id = resolve_flexible_reference(entry) - if resolved_id: - resolved_entries.append((resolved_id, name)) - - self.ctx.format_helper.append_line( - lines, 2, f"{cpp_helper}({len(resolved_entries)})," - ) - fixed_bytes = int(field.string_fixed_bytes or 12) - for obj_id, name in resolved_entries: - self.ctx.format_helper.append_line(lines, 3, f"{obj_id.upper()},") - self.ctx.format_helper.append_words( - lines, - self.ctx.format_helper.pack_fixed_string( - str(name), fixed_bytes - ), - level=3, - ) - return lines - def generate_generic_field( - self, value: Any, field: ConfigField + self, + value: Any, + field: ConfigField, + hook: ProtoFieldHook | None = None, ) -> list[str]: - """Generate lines for one generic custom protocol field.""" + """Generate lines for one custom protocol field. + + The owning handler's ``emit_config_field_cpp`` hook is offered the + field first (protocol-specific value-types); anything it declines + falls back to the shared generic value-types below. + """ lines: list[str] = [] cpp_helper = field.cpp_helper value_type = field.value_type @@ -115,6 +95,9 @@ def generate_generic_field( if not cpp_helper: return lines + if hook is not None and hook(lines, field, value, self.ctx): + return lines + if value_type == "string": packed_words = self.ctx.format_helper.pack_string(str(value)) self.ctx.format_helper.append_line( @@ -123,11 +106,6 @@ def generate_generic_field( self.ctx.format_helper.append_words(lines, packed_words, level=3) return lines - if value_type == "nxscope_iobind2": - return self.generate_nxscope_iobind2_field( - value, field, cpp_helper - ) - if value_type == "string_array": size_value, all_words = self.pack_string_array_field(value, field) self.ctx.format_helper.append_line( @@ -168,13 +146,16 @@ def generate( lines, 1, f"{macro_name}, {config_count}," ) + handler = PROTO_HANDLER_REGISTRY.get(proto_type) + hook = getattr(handler, "emit_config_field_cpp", None) + for field in fields: if field.nested: continue # pragma: no cover if field.name not in config: continue # pragma: no cover lines.extend( - self.generate_generic_field(config[field.name], field) + self.generate_generic_field(config[field.name], field, hook) ) if uses_standard and bindings: diff --git a/src/dawnpy/descriptor/handlers/_base.py b/src/dawnpy/descriptor/handlers/_base.py index 5b7e3db..c28ffb3 100644 --- a/src/dawnpy/descriptor/handlers/_base.py +++ b/src/dawnpy/descriptor/handlers/_base.py @@ -101,6 +101,10 @@ class ProgHandler(Protocol): nuttx_requirements: tuple[str, ...] nuttx_value_requirements: tuple[tuple[str, str, int], ...] + def __getattr__(self, name: str) -> Any: + """Allow optional per-handler C++ generator hooks.""" + raise NotImplementedError + @staticmethod def config_fields() -> list[ConfigField]: """Return the user-facing config schema for this prog type.""" diff --git a/src/dawnpy/descriptor/handlers/_proto_nxscope_common.py b/src/dawnpy/descriptor/handlers/_proto_nxscope_common.py index 146fd3c..9051171 100644 --- a/src/dawnpy/descriptor/handlers/_proto_nxscope_common.py +++ b/src/dawnpy/descriptor/handlers/_proto_nxscope_common.py @@ -9,6 +9,8 @@ plus its variant-specific fields in its own handler. """ +from typing import Any + from dawnpy.descriptor.definitions.type_info import ConfigField from dawnpy.descriptor.encoding.proto_runtime import _ProtoSerializeContext from dawnpy.descriptor.encoding.words import ( @@ -74,6 +76,45 @@ def encode_nxscope_iobind2(ctx: _ProtoSerializeContext) -> None: ) +def nxscope_emit_config_field_cpp( + lines: list[str], field: ConfigField, value: Any, ctx: Any +) -> bool: + """Emit the ``nxscope_iobind2`` C++ config field; return whether handled. + + This is the per-handler C++ source hook counterpart to + :func:`encode_nxscope_iobind2` (the binary path). ``ctx`` is the + protocol generator context (provides ``format_helper``). + """ + if field.value_type != "nxscope_iobind2": + return False + + entries = value if isinstance(value, list) else [] + resolved_entries: list[tuple[str, str]] = [] + for entry in entries: + if isinstance(entry, str): + name = "" + elif isinstance(entry, dict): + name = entry.get("name", "") + else: + continue + resolved_id = resolve_flexible_reference(entry) + if resolved_id: + resolved_entries.append((resolved_id, name)) + + ctx.format_helper.append_line( + lines, 2, f"{field.cpp_helper}({len(resolved_entries)})," + ) + fixed_bytes = int(field.string_fixed_bytes or 12) + for obj_id, name in resolved_entries: + ctx.format_helper.append_line(lines, 3, f"{obj_id.upper()},") + ctx.format_helper.append_words( + lines, + ctx.format_helper.pack_fixed_string(str(name), fixed_bytes), + level=3, + ) + return True + + def iobind2_field(cpp_class: str) -> ConfigField: """Return the iobind2 schema row, bound to the variant's cpp_class.""" return ConfigField( diff --git a/src/dawnpy/descriptor/handlers/proto_nxscope_dummy.py b/src/dawnpy/descriptor/handlers/proto_nxscope_dummy.py index 3acfbb5..2a6e250 100644 --- a/src/dawnpy/descriptor/handlers/proto_nxscope_dummy.py +++ b/src/dawnpy/descriptor/handlers/proto_nxscope_dummy.py @@ -13,6 +13,7 @@ encode_nxscope_iobind2, iobind2_field, nxscope_allocation_rows, + nxscope_emit_config_field_cpp, resolve_nxscope_bindings, ) @@ -34,6 +35,7 @@ defaults: dict[str, int] = {} fixed_string_bytes: dict[str, int] = {"nxscope_name": 12} resolve_bindings = resolve_nxscope_bindings +emit_config_field_cpp = nxscope_emit_config_field_cpp def allocation_rows(proto: Any) -> list[list[str]]: # pragma: no cover diff --git a/src/dawnpy/descriptor/handlers/proto_nxscope_serial.py b/src/dawnpy/descriptor/handlers/proto_nxscope_serial.py index 6ad4516..a3fc59d 100644 --- a/src/dawnpy/descriptor/handlers/proto_nxscope_serial.py +++ b/src/dawnpy/descriptor/handlers/proto_nxscope_serial.py @@ -14,6 +14,7 @@ encode_nxscope_iobind2, iobind2_field, nxscope_allocation_rows, + nxscope_emit_config_field_cpp, resolve_nxscope_bindings, ) @@ -37,6 +38,7 @@ defaults: dict[str, int] = {} fixed_string_bytes: dict[str, int] = {"nxscope_name": 12} resolve_bindings = resolve_nxscope_bindings +emit_config_field_cpp = nxscope_emit_config_field_cpp def allocation_rows(proto: Any) -> list[list[str]]: diff --git a/src/dawnpy/descriptor/handlers/proto_nxscope_udp.py b/src/dawnpy/descriptor/handlers/proto_nxscope_udp.py index 086ab69..259291c 100644 --- a/src/dawnpy/descriptor/handlers/proto_nxscope_udp.py +++ b/src/dawnpy/descriptor/handlers/proto_nxscope_udp.py @@ -14,6 +14,7 @@ encode_nxscope_iobind2, iobind2_field, nxscope_allocation_rows, + nxscope_emit_config_field_cpp, resolve_nxscope_bindings, ) @@ -37,6 +38,7 @@ defaults: dict[str, int] = {} fixed_string_bytes: dict[str, int] = {"nxscope_name": 12} resolve_bindings = resolve_nxscope_bindings +emit_config_field_cpp = nxscope_emit_config_field_cpp def allocation_rows(proto: Any) -> list[list[str]]: # pragma: no cover diff --git a/tests/descriptor/handlers/test_io_common.py b/tests/descriptor/handlers/test_io_common.py index a7e16e7..d8eb3a8 100644 --- a/tests/descriptor/handlers/test_io_common.py +++ b/tests/descriptor/handlers/test_io_common.py @@ -18,6 +18,63 @@ class TestIoHandlers: + def test_generate_io_config_field_hook(self, generator, monkeypatch): + """A handler's emit_config_field_cpp owns its type-specific field. + + Fields the hook declines fall back to the generic loop. + """ + from dawnpy.descriptor.handlers import io_dummy + + def hook(lines, field_def, obj, ctx): + if field_def.value_type != "custom_io": + return False + ctx.format_helper.append_line( + lines, 2, f"{field_def.cpp_helper}()," + ) + ctx.format_helper.append_line(lines, 3, "42,") + return True + + monkeypatch.setattr( + io_dummy, "emit_config_field_cpp", hook, raising=False + ) + monkeypatch.setattr( + generator.config_loader, + "get_io_config_fields", + lambda io_type: [ + ConfigField( + name="foo", + value_type="custom_io", + cpp_helper="CIODummy::cfgIdFoo", + ), + ConfigField( + name="bar", + value_type="int", + cpp_helper="CIODummy::cfgIdBar", + ), + ], + ) + obj = IoObject( + obj_id="d1", + io_type="dummy", + dtype="uint32", + instance=1, + timestamp=False, + rw=False, + notify=False, + tags=[], + config={"foo": 1, "bar": 7}, + subtype=None, + variant=None, + ) + lines = generator._generate_io_config("D1", obj) + joined = "\n".join(lines) + # hook owned 'foo' + assert "CIODummy::cfgIdFoo()," in joined + assert " 42," in lines + # hook declined 'bar' -> generic scalar emission + assert "CIODummy::cfgIdBar()," in joined + assert " 7," in lines + def test_generate_io_config_no_config(self, generator): """Test generating IO config with no configuration.""" obj = IoObject( diff --git a/tests/descriptor/handlers/test_prog_common.py b/tests/descriptor/handlers/test_prog_common.py index 77e0ddc..5c28468 100644 --- a/tests/descriptor/handlers/test_prog_common.py +++ b/tests/descriptor/handlers/test_prog_common.py @@ -19,6 +19,25 @@ pytestmark = pytest.mark.usefixtures("source_free_headers") +def test_generate_prog_config_uses_handler_generate_cpp(monkeypatch): + """A prog handler's generate_cpp fully owns its config block.""" + from dawnpy.descriptor.handlers import prog_dummy + + def gen_cpp(macro_name, obj, ctx): + assert ctx.format_helper is not None + return [f" {macro_name}, CUSTOM,"] + + monkeypatch.setattr(prog_dummy, "generate_cpp", gen_cpp, raising=False) + + generator = DescriptorGenerator() + obj = ProgramObject.from_spec( + {"id": "prog1", "type": "dummy", "instance": 1, "config": {}} + ) + assert obj is not None + lines = generator._generate_prog_config("PROG1", obj) + assert lines == [" PROG1, CUSTOM,"] + + def test_generate_prog_config_handles_id_single(monkeypatch): generator = DescriptorGenerator() monkeypatch.setattr( diff --git a/tests/descriptor/handlers/test_proto_generic.py b/tests/descriptor/handlers/test_proto_generic.py index df7685f..5ee41a2 100644 --- a/tests/descriptor/handlers/test_proto_generic.py +++ b/tests/descriptor/handlers/test_proto_generic.py @@ -375,19 +375,47 @@ def test_generate_generic_proto_field_without_cpp_helper(self): proto_gen = generator._protocol_config_generator() assert proto_gen._generic.generate_generic_field(10, field) == [] - def test_generate_nxscope_iobind2_field_wrapper(self): - """Test nxscope_iobind2 wrapper delegates and formats output.""" - generator = DescriptorGenerator() - proto_gen = generator._protocol_config_generator()._generic - lines = proto_gen.generate_nxscope_iobind2_field( - value=[{"id": "io1", "name": "chan1"}, "io2"], - field=ConfigField(name="iobind2", string_fixed_bytes=8), + def test_nxscope_handler_emit_config_field_cpp(self): + """The nxscope handler owns nxscope_iobind2 C++ emission.""" + from types import SimpleNamespace + + from dawnpy.descriptor.handlers.proto_nxscope_serial import ( + emit_config_field_cpp, + ) + from dawnpy.descriptor.support.formatting import ( + DescriptorFormatHelper, + ) + + ctx = SimpleNamespace(format_helper=DescriptorFormatHelper()) + field = ConfigField( + name="iobind2", cpp_helper="CProtoNxscope::cfgIdIOBind2", + value_type="nxscope_iobind2", + string_fixed_bytes=8, ) + lines: list[str] = [] + handled = emit_config_field_cpp( + lines, field, [{"id": "io1", "name": "chan1"}, "io2"], ctx + ) + assert handled is True assert lines[0] == " CProtoNxscope::cfgIdIOBind2(2)," assert " IO1," in lines assert " IO2," in lines + def test_nxscope_handler_declines_other_value_type(self): + """The nxscope hook returns False for non-nxscope fields.""" + from types import SimpleNamespace + + from dawnpy.descriptor.handlers.proto_nxscope_serial import ( + emit_config_field_cpp, + ) + + ctx = SimpleNamespace(format_helper=None) + field = ConfigField(name="path", cpp_helper="H", value_type="string") + lines: list[str] = [] + assert emit_config_field_cpp(lines, field, "x", ctx) is False + assert lines == [] + def test_count_generic_proto_config_items(self): """Test counting with standard bindings and nested fields.""" generator = DescriptorGenerator()