diff --git a/src/fpy/state.py b/src/fpy/state.py index aa0be9c..999480f 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,14 @@ 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 (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 + ) + 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..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, @@ -275,6 +276,98 @@ 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() + 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() + 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() + 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 # ============================================================================