From ef03e3c3ae4d1c03b1d0e452110de75e023d8453 Mon Sep 17 00:00:00 2001 From: Zimri Leisher Date: Fri, 17 Jul 2026 18:24:01 -0700 Subject: [PATCH 1/3] Read FW_SERIALIZE_TRUE/FALSE_VALUE from dict Co-Authored-By: Claude Opus 4.8 --- src/fpy/state.py | 14 +++++ src/fpy/types.py | 12 +++- test/fpy/test_compiler_config.py | 95 ++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/fpy/state.py b/src/fpy/state.py index aa0be9c..c9256ea 100644 --- a/src/fpy/state.py +++ b/src/fpy/state.py @@ -3,6 +3,7 @@ from typing import Union from dataclasses import dataclass, field +import fpy.types from fpy.dictionary import json_default_to_fpy_value, load_dictionary from fpy.error import CompileError, CompileWarning, DictionaryError, WarningType from fpy.ir import Ir, IrLabel @@ -38,6 +39,8 @@ BOOL, CHECK_STATE, CMD_RESPONSE, + DEFAULT_FW_SERIALIZE_FALSE_VALUE, + DEFAULT_FW_SERIALIZE_TRUE_VALUE, DEFAULT_MAX_DIRECTIVES_COUNT, DEFAULT_MAX_DIRECTIVE_SIZE, FLAGS_TYPE, @@ -601,6 +604,17 @@ def _const_int(key: str, default: int) -> int: ), f"Expected int for constant {key}, got {type(val.val)}" return val.val + # Override the live boolean wire-format constants from the dictionary. + # FpyValue.serialize() is stateless and reads these module globals directly, + # so we set them here (in the uncached path, run once per compile) rather + # than threading them through every serialize() call site. + fpy.types.FW_SERIALIZE_TRUE_VALUE = _const_int( + "FW_SERIALIZE_TRUE_VALUE", DEFAULT_FW_SERIALIZE_TRUE_VALUE + ) + fpy.types.FW_SERIALIZE_FALSE_VALUE = _const_int( + "FW_SERIALIZE_FALSE_VALUE", DEFAULT_FW_SERIALIZE_FALSE_VALUE + ) + # Make copies of the scopes since we'll mutate them during compilation # (e.g., adding user-defined functions to callable_scope, variables to values_scope) # if we don't make copies, then the lru cache will return the modified versions, causing diff --git a/src/fpy/types.py b/src/fpy/types.py index 1be18bc..80a2390 100644 --- a/src/fpy/types.py +++ b/src/fpy/types.py @@ -34,9 +34,15 @@ COMPILER_MAX_STRING_SIZE = 128 -# FPP wire-format constants for boolean serialization -FW_SERIALIZE_TRUE_VALUE = 0xFF -FW_SERIALIZE_FALSE_VALUE = 0x00 +# FPP wire-format constants for boolean serialization. +# The live FW_SERIALIZE_* values may be overridden from the dictionary at +# compile time (see get_base_compile_state); the DEFAULT_* values are the +# framework fallbacks used when the dictionary does not define them. +DEFAULT_FW_SERIALIZE_TRUE_VALUE = 0xFF +DEFAULT_FW_SERIALIZE_FALSE_VALUE = 0x00 + +FW_SERIALIZE_TRUE_VALUE = DEFAULT_FW_SERIALIZE_TRUE_VALUE +FW_SERIALIZE_FALSE_VALUE = DEFAULT_FW_SERIALIZE_FALSE_VALUE class TypeKind(str, Enum): diff --git a/test/fpy/test_compiler_config.py b/test/fpy/test_compiler_config.py index 91c90ca..a875ccf 100644 --- a/test/fpy/test_compiler_config.py +++ b/test/fpy/test_compiler_config.py @@ -275,6 +275,101 @@ def test_within_custom_limit_succeeds(): _clear_caches() +# ============================================================================ +# FW_SERIALIZE_TRUE_VALUE / FW_SERIALIZE_FALSE_VALUE loading tests +# ============================================================================ + + +def fw_serialize_constant(name: str, value: int) -> dict: + """Build a dictionary constant descriptor for a FW_SERIALIZE_* value.""" + return { + "kind": "constant", + "qualifiedName": name, + "type": {"name": "U64", "kind": "integer", "size": 64, "signed": False}, + "value": value, + "annotation": f"Custom {name} for testing", + } + + +def test_load_fw_serialize_from_default_dictionary(): + """Test that boolean wire-format values are loaded from the standard dictionary.""" + _clear_caches() + import fpy.types + from fpy.types import BOOL, FpyValue + + get_base_compile_state(DEFAULT_DICTIONARY, {}) + + # The RefTopologyDictionary.json has FW_SERIALIZE_TRUE_VALUE=255, FALSE=0 + assert fpy.types.FW_SERIALIZE_TRUE_VALUE == 0xFF + assert fpy.types.FW_SERIALIZE_FALSE_VALUE == 0x00 + assert FpyValue(BOOL, True).serialize() == b"\xff" + assert FpyValue(BOOL, False).serialize() == b"\x00" + + +def test_custom_fw_serialize_values(): + """Test that custom boolean wire-format values from the dictionary are used.""" + _clear_caches() + import fpy.types + from fpy.types import ( + BOOL, + DEFAULT_FW_SERIALIZE_FALSE_VALUE, + DEFAULT_FW_SERIALIZE_TRUE_VALUE, + FpyValue, + ) + + dict_path = create_test_dictionary( + [ + fw_serialize_constant("FW_SERIALIZE_TRUE_VALUE", 1), + fw_serialize_constant("FW_SERIALIZE_FALSE_VALUE", 2), + ] + ) + + try: + get_base_compile_state(dict_path, {}) + assert fpy.types.FW_SERIALIZE_TRUE_VALUE == 1 + assert fpy.types.FW_SERIALIZE_FALSE_VALUE == 2 + # Booleans now serialize using the dictionary-provided wire values. + assert FpyValue(BOOL, True).serialize() == b"\x01" + assert FpyValue(BOOL, False).serialize() == b"\x02" + finally: + # Restore the framework defaults so we don't leak into other tests. + fpy.types.FW_SERIALIZE_TRUE_VALUE = DEFAULT_FW_SERIALIZE_TRUE_VALUE + fpy.types.FW_SERIALIZE_FALSE_VALUE = DEFAULT_FW_SERIALIZE_FALSE_VALUE + Path(dict_path).unlink() + _clear_caches() + + +def test_missing_fw_serialize_use_defaults(): + """Test that missing FW_SERIALIZE constants fall back to framework defaults.""" + _clear_caches() + import fpy.types + from fpy.types import ( + DEFAULT_FW_SERIALIZE_FALSE_VALUE, + DEFAULT_FW_SERIALIZE_TRUE_VALUE, + ) + + dict_path = create_test_dictionary([]) + + # Remove the FW_SERIALIZE constants from the dictionary. + with open(dict_path, "r") as f: + dict_json = json.load(f) + dict_json["constants"] = [ + c + for c in dict_json.get("constants", []) + if not c.get("qualifiedName", "").startswith("FW_SERIALIZE_") + ] + with open(dict_path, "w") as f: + json.dump(dict_json, f) + + try: + get_base_compile_state(dict_path, {}) + assert fpy.types.FW_SERIALIZE_TRUE_VALUE == DEFAULT_FW_SERIALIZE_TRUE_VALUE + assert fpy.types.FW_SERIALIZE_FALSE_VALUE == DEFAULT_FW_SERIALIZE_FALSE_VALUE + finally: + Path(dict_path).unlink() + _clear_caches() + + # ============================================================================ # TimeBase dictionary loading tests # ============================================================================ From 7516165d188faa17126de266f91a13909e48676e Mon Sep 17 00:00:00 2001 From: Zimri Leisher Date: Fri, 17 Jul 2026 18:29:36 -0700 Subject: [PATCH 2/3] Simplify comment to one line Co-Authored-By: Claude Opus 4.8 --- src/fpy/state.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/fpy/state.py b/src/fpy/state.py index c9256ea..999480f 100644 --- a/src/fpy/state.py +++ b/src/fpy/state.py @@ -604,10 +604,7 @@ def _const_int(key: str, default: int) -> int: ), f"Expected int for constant {key}, got {type(val.val)}" return val.val - # Override the live boolean wire-format constants from the dictionary. - # FpyValue.serialize() is stateless and reads these module globals directly, - # so we set them here (in the uncached path, run once per compile) rather - # than threading them through every serialize() call site. + # Override the live boolean wire-format constants (read directly by the stateless FpyValue.serialize()) from the dictionary. fpy.types.FW_SERIALIZE_TRUE_VALUE = _const_int( "FW_SERIALIZE_TRUE_VALUE", DEFAULT_FW_SERIALIZE_TRUE_VALUE ) From 9ff67135c998be946748512e343d62c52414c652 Mon Sep 17 00:00:00 2001 From: Zimri Leisher Date: Fri, 17 Jul 2026 18:34:16 -0700 Subject: [PATCH 3/3] Move import fpy.types to top of test file Co-Authored-By: Claude Opus 4.8 --- test/fpy/test_compiler_config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/fpy/test_compiler_config.py b/test/fpy/test_compiler_config.py index a875ccf..dabfd83 100644 --- a/test/fpy/test_compiler_config.py +++ b/test/fpy/test_compiler_config.py @@ -14,6 +14,7 @@ import pytest import fpy.error +import fpy.types from fpy.compiler import ( text_to_ast, analyze_ast, @@ -294,7 +295,6 @@ def fw_serialize_constant(name: str, value: int) -> dict: def test_load_fw_serialize_from_default_dictionary(): """Test that boolean wire-format values are loaded from the standard dictionary.""" _clear_caches() - import fpy.types from fpy.types import BOOL, FpyValue get_base_compile_state(DEFAULT_DICTIONARY, {}) @@ -309,7 +309,6 @@ def test_load_fw_serialize_from_default_dictionary(): def test_custom_fw_serialize_values(): """Test that custom boolean wire-format values from the dictionary are used.""" _clear_caches() - import fpy.types from fpy.types import ( BOOL, DEFAULT_FW_SERIALIZE_FALSE_VALUE, @@ -342,7 +341,6 @@ def test_custom_fw_serialize_values(): def test_missing_fw_serialize_use_defaults(): """Test that missing FW_SERIALIZE constants fall back to framework defaults.""" _clear_caches() - import fpy.types from fpy.types import ( DEFAULT_FW_SERIALIZE_FALSE_VALUE, DEFAULT_FW_SERIALIZE_TRUE_VALUE,