Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
105 changes: 98 additions & 7 deletions aas_core_codegen/cpp/lib/_generate_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,85 @@ def _generate_string_helper_definitions(
# the methods of the types can use it. It is public so that the clients can rely
# on it, and so that we can unit-test it.


#: Declarations of the predicates checking the safe integer range
IS_SAFE_INTEGER_DECLARATIONS: Final[Sequence[Stripped]] = [
Stripped(
"""\
/**
* \\brief Check that the integer \\p value lies in the safe integer range
* [-2^53 + 1, 2^53 - 1].
*
* JSON knows a single numeric type, which many readers parse into a 64-bit
* floating-point number. Outside the safe integer range, such a reader might
* quietly get a different integer than the one which was written.
*
* The verification, the JSON and XML de/serialization and ParseSafeInt all
* use this check, so that an instance which passes the verification can be
* serialized to JSON.
*
* \\param value to be checked
* \\return true if \\p value lies in the range
*/
bool IsSafeInteger(int64_t value);"""
),
Stripped(
"""\
/**
* \\brief Check that the unsigned integer \\p value lies in the safe integer range
* [-2^53 + 1, 2^53 - 1].
*
* \\param value to be checked
* \\return true if \\p value lies in the range
*/
bool IsSafeInteger(uint64_t value);"""
),
Stripped(
"""\
/**
* \\brief Check that the whole number \\p value lies in the safe integer range
* [-2^53 + 1, 2^53 - 1].
*
* We check only the range. The caller has to check that \\p value is whole.
*
* \\param value to be checked
* \\return true if \\p value lies in the range, false for a not-a-number
*/
bool IsSafeInteger(double value);"""
),
]

#: Definitions of the predicates checking the safe integer range
IS_SAFE_INTEGER_DEFINITIONS: Final[Sequence[Stripped]] = [
Stripped(
f"""\
bool IsSafeInteger(int64_t value) {{
{I}return (
{II}-9007199254740991LL <= value
{II}&& value <= 9007199254740991LL
{I});
}}"""
),
Stripped(
f"""\
bool IsSafeInteger(uint64_t value) {{
{I}return value <= 9007199254740991ULL;
}}"""
),
Stripped(
f"""\
bool IsSafeInteger(double value) {{
{I}// NOTE (mristin):
{I}// A not-a-number fails both comparisons, so it lies outside the range.
{I}return (
{II}-9007199254740991.0 <= value
{II}&& value <= 9007199254740991.0
{I});
}}"""
),
]


#: Name of the helper function to parse the safe integers
PARSE_SAFE_INT_NAME: Final[Identifier] = Identifier("ParseSafeInt")

Expand All @@ -847,8 +926,13 @@ def _generate_string_helper_definitions(
* transpilation. We accept only an optional sign followed by the ASCII digits,
* and only the safe integers, <em>i.e.</em>, the integers within
* <code>-(2^53 - 1)</code> and <code>2^53 - 1</code>, which a double-precision
* floating-point number represents exactly. This way, all the SDKs behave
* the same.
* floating-point number represents exactly.
*
* We check the safe range even though an <code>int64_t</code> could hold
* larger integers. TypeScript represents the integers as <code>number</code>,
* which holds them exactly only within the safe range, and all the SDKs check
* the same range so that the invariants which parse a text behave the same
* everywhere.
*
* \\param text to be parsed
* \\return parsed integer
Expand All @@ -862,9 +946,6 @@ def _generate_string_helper_definitions(
PARSE_SAFE_INT_DEFINITION = Stripped(
f"""\
int64_t ParseSafeInt(const std::wstring& text) {{
{I}// NOTE: This is 2^53 - 1.
{I}const int64_t kMaxSafeInteger = 9007199254740991LL;

{I}size_t offset = 0;
{I}bool negative = false;
{I}if (!text.empty() && (text[0] == L'-' || text[0] == L'+')) {{
Expand Down Expand Up @@ -892,7 +973,7 @@ def _generate_string_helper_definitions(
{II}// NOTE: The value never overflows as we check it after each digit, and
{II}// ten times the largest safe integer fits into int64_t.
{II}value = value * 10 + static_cast<int64_t>(character - L'0');
{II}if (value > kMaxSafeInteger) {{
{II}if (!IsSafeInteger(value)) {{
{III}throw std::invalid_argument(
{IIII}"Expected a safe integer, but got a text out of its range: "
{IIII}+ WstringToUtf8(text)
Expand Down Expand Up @@ -1346,7 +1427,7 @@ def generate_header(
or intermediate_uses.string_slicing(type_inference)
or intermediate_uses.string_find_call(type_inference)
or intermediate_uses.modulo(symbol_table)
or intermediate_uses.int_call(symbol_table)
or intermediate_uses.safe_integer_checks(symbol_table)
):
extra_std_includes.append("#include <cstdint>")

Expand Down Expand Up @@ -1648,6 +1729,11 @@ def generate_header(
if intermediate_uses.modulo(symbol_table)
else []
),
*(
IS_SAFE_INTEGER_DECLARATIONS
if intermediate_uses.safe_integer_checks(symbol_table)
else []
),
*(
[PARSE_SAFE_INT_DECLARATION]
if intermediate_uses.int_call(symbol_table)
Expand Down Expand Up @@ -1956,6 +2042,11 @@ def generate_implementation(
else []
),
*([FLOOR_MOD_DEFINITION] if intermediate_uses.modulo(symbol_table) else []),
*(
IS_SAFE_INTEGER_DEFINITIONS
if intermediate_uses.safe_integer_checks(symbol_table)
else []
),
*(
[PARSE_SAFE_INT_DEFINITION]
if intermediate_uses.int_call(symbol_table)
Expand Down
129 changes: 115 additions & 14 deletions aas_core_codegen/cpp/lib/_generate_json_value_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

# NOTE (mristin):
# This module is private (it is emitted to ``src/``, never installed alongside
# the public headers in ``include/``), included only by ``verification.cpp``.
# the public headers in ``include/``), included by ``verification.cpp``, and by
# ``jsonization.cpp`` for the shared ``CheckJsonAbleNumber``.
# It provides ``JsonValueVerificator``, a hand-written ``impl::IVerificator``
# (see ``verification.hpp``) which exhaustively walks a ``nlohmann::json``
# value with an explicit, runtime stack -- unlike every other verificator in
Expand All @@ -29,6 +30,115 @@
)


def _generate_check_json_able_number_declaration() -> Stripped:
"""Generate the declaration of the check of a number in a JSON-able value."""
return Stripped(
"""\
/**
* \\brief Check that the number \\p json is a number of a JSON-able value.
*
* JSON knows a single numeric type, which all the other SDKs read into
* a 64-bit floating-point number. Hence a JSON-able number has to be finite,
* and an integer has to lie in the safe integer range [-2^53 + 1, 2^53 - 1],
* as for the integer properties of the model.
*
* The verification, the de-serialization and the serialization of a JSON-able
* value all use this check, so that they agree on what a JSON-able number is.
*
* \\param json number to be checked
* \\return the cause why \\p json is not JSON-able, if it is not
*/
common::optional<std::wstring> CheckJsonAbleNumber(
const nlohmann::json& json
);"""
)


def _generate_check_json_able_number_implementation() -> Stripped:
"""
Generate the check of a number in a JSON-able value.

The verification, the de-serialization and the serialization of a JSON-able
value all use it, so that they agree on what a JSON-able number is.
"""
return Stripped(
f"""\
namespace {{
/**
* \\brief Describe that the \\p integer of a JSON-able value lies outside
* the safe integer range.
*
* \\param integer textual representation of the integer
* \\return the cause of the error
*/
std::wstring DescribeJsonAbleIntegerOutsideSafeRange(
{I}const std::wstring& integer
) {{
{I}return common::Concat(
{II}L"Expected a JSON-able value, but got the integer ",
{II}integer,
{II}L", which is outside the safe integer range [-2^53 + 1, 2^53 - 1]"
{I});
}}
}} // namespace

/**
* \\brief Check that the number \\p json is a number of a JSON-able value.
*
* JSON knows a single numeric type, which all the other SDKs read into
* a 64-bit floating-point number. Hence a JSON-able number has to be finite,
* and an integer has to lie in the safe integer range [-2^53 + 1, 2^53 - 1],
* as for the integer properties of the model -- otherwise a reader might
* quietly get a different number than the one which was given.
*
* \\param json number to be checked
* \\return the cause why \\p json is not JSON-able, if it is not
*/
common::optional<std::wstring> CheckJsonAbleNumber(
{I}const nlohmann::json& json
) {{
{I}switch (json.type()) {{
{II}case nlohmann::json::value_t::number_integer: {{
{III}const int64_t integer = json.get<int64_t>();
{III}if (!common::IsSafeInteger(integer)) {{
{IIII}return DescribeJsonAbleIntegerOutsideSafeRange(std::to_wstring(integer));
{III}}}
{III}return common::nullopt;
{II}}}

{II}case nlohmann::json::value_t::number_unsigned: {{
{III}const uint64_t integer = json.get<uint64_t>();
{III}if (!common::IsSafeInteger(integer)) {{
{IIII}return DescribeJsonAbleIntegerOutsideSafeRange(std::to_wstring(integer));
{III}}}
{III}return common::nullopt;
{II}}}

{II}case nlohmann::json::value_t::number_float: {{
{III}// NOTE (mristin):
{III}// JSON knows neither an infinity nor a not-a-number, so we refuse both
{III}// instead of silently writing them out as ``null``. The caller gives us
{III}// an ``nlohmann::json`` which may well have been constructed
{III}// programmatically, so a non-finite number can reach us even though no
{III}// conformant parser would ever produce one.
{III}const double number = json.get<double>();
{III}if (!std::isfinite(number)) {{
{IIII}return common::Concat(
{IIIII}L"Expected a JSON-able value, but got the number ",
{IIIII}std::to_wstring(number),
{IIIII}L", which is neither finite nor representable in JSON"
{IIII});
{III}}}
{III}return common::nullopt;
{II}}}

{II}default:
{III}return common::nullopt;
{I}}}
}}"""
)


def _generate_json_value_verificator_declaration() -> List[Stripped]:
"""Generate the declaration of ``JsonValueVerificator``."""
return [
Expand Down Expand Up @@ -175,6 +285,7 @@ class JsonValueVerificator : public impl::IVerificator {{
def _generate_json_value_verificator_implementation() -> List[Stripped]:
"""Generate the implementation of ``JsonValueVerificator``."""
return [
_generate_check_json_able_number_implementation(),
Stripped(
f"""\
namespace {{
Expand All @@ -198,19 +309,8 @@ def _generate_json_value_verificator_implementation() -> List[Stripped]:
{II});
{I}}}

{I}// NOTE (mristin):
{I}// JSON knows neither an infinity nor a not-a-number, so neither is
{I}// a JSON-able value, even though ``nlohmann::json`` will happily hold
{I}// one if it was constructed programmatically. This mirrors what the
{I}// jsonization refuses to serialize.
{I}if (value.is_number_float() && !std::isfinite(value.get<double>())) {{
{II}return common::make_optional<std::wstring>(
{III}common::Concat(
{IIII}L"Expected a JSON-able value, but got the number ",
{IIII}std::to_wstring(value.get<double>()),
{IIII}L", which is neither finite nor representable in JSON"
{III})
{II});
{I}if (value.is_number()) {{
{II}return CheckJsonAbleNumber(value);
{I}}}

{I}return common::nullopt;
Expand Down Expand Up @@ -491,6 +591,7 @@ def generate_header(library_namespace: Stripped) -> str:
),
cpp_common.generate_namespace_opening(library_namespace),
Stripped(f"namespace {cpp_common.VERIFICATION_NAMESPACE} {{"),
_generate_check_json_able_number_declaration(),
*_generate_json_value_verificator_declaration(),
Stripped(f"}} // namespace {cpp_common.VERIFICATION_NAMESPACE}"),
cpp_common.generate_namespace_closing(library_namespace),
Expand Down
Loading
Loading