diff --git a/compiler/back_end/cpp/BUILD b/compiler/back_end/cpp/BUILD index 0ea2249e..2314c069 100644 --- a/compiler/back_end/cpp/BUILD +++ b/compiler/back_end/cpp/BUILD @@ -204,6 +204,18 @@ emboss_cc_test( ], ) +emboss_cc_test( + name = "int128_sizes_test", + srcs = [ + "testcode/int128_sizes_test.cc", + ], + deps = [ + "//runtime/cpp:cpp_utils", + "//testdata:int128_sizes_emboss", + "@com_google_googletest//:gtest_main", + ], +) + emboss_cc_test( name = "float_test", srcs = [ diff --git a/compiler/back_end/cpp/testcode/int128_sizes_test.cc b/compiler/back_end/cpp/testcode/int128_sizes_test.cc new file mode 100644 index 00000000..3e18f878 --- /dev/null +++ b/compiler/back_end/cpp/testcode/int128_sizes_test.cc @@ -0,0 +1,427 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for 128-bit integer support in generated Emboss code. +// This file tests structures defined in int128_sizes.emb with UInt and Int +// fields larger than 64 bits. + +#include + +#include +#include + +#include "gtest/gtest.h" +#include "runtime/cpp/emboss_defines.h" + +#if EMBOSS_HAS_INT128 +#include "testdata/int128_sizes.emb.h" + +namespace emboss { +namespace test { +namespace { + +// Test data for UInt128Sizes (100 bytes total) +alignas(16) static const ::std::uint8_t kUInt128Sizes[100] = { + // 0:9 nine_byte (72 bits) = 0x090807060504030201 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + // 9:19 ten_byte (80 bits) + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, + // 19:30 eleven_byte (88 bits) + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + // 30:42 twelve_byte (96 bits) + 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, + // 42:55 thirteen_byte (104 bits) + 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, + // 55:69 fourteen_byte (112 bits) + 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, + 0x44, 0x45, + // 69:84 fifteen_byte (120 bits) + 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, + 0x52, 0x53, 0x54, + // 84:100 sixteen_byte (128 bits) + 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, + 0x61, 0x62, 0x63, 0x64, +}; + +TEST(UInt128SizesView, CanReadSizes) { + auto view = MakeUInt128SizesView(kUInt128Sizes, sizeof kUInt128Sizes); + EXPECT_TRUE(view.Ok()); + + // Nine byte (72 bits) - little endian + __uint128_t expected_nine = + (static_cast<__uint128_t>(0x09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected_nine, view.nine_byte().Read()); + EXPECT_EQ(16U, sizeof(view.nine_byte().Read())); + + // Ten byte (80 bits) + __uint128_t expected_ten = + (static_cast<__uint128_t>(0x1312UL) << 64) | + static_cast<__uint128_t>(0x11100f0e0d0c0b0aUL); + EXPECT_EQ(expected_ten, view.ten_byte().Read()); + + // Sixteen byte (128 bits) + __uint128_t expected_sixteen = + (static_cast<__uint128_t>(0x6463626160'5f5e5dUL) << 64) | + static_cast<__uint128_t>(0x5c5b5a5958'575655UL); + EXPECT_EQ(expected_sixteen, view.sixteen_byte().Read()); +} + +TEST(UInt128SizesWriter, CanWriteSizes) { + ::std::uint8_t buffer[sizeof kUInt128Sizes] = {}; + auto writer = UInt128SizesWriter(buffer, sizeof buffer); + + // Write nine byte value + __uint128_t nine_value = + (static_cast<__uint128_t>(0xffUL) << 64) | + static_cast<__uint128_t>(0xfedcba9876543210UL); + writer.nine_byte().Write(nine_value); + + // Verify written bytes (little-endian) + EXPECT_EQ(0x10, buffer[0]); + EXPECT_EQ(0x32, buffer[1]); + EXPECT_EQ(0x54, buffer[2]); + EXPECT_EQ(0x76, buffer[3]); + EXPECT_EQ(0x98, buffer[4]); + EXPECT_EQ(0xba, buffer[5]); + EXPECT_EQ(0xdc, buffer[6]); + EXPECT_EQ(0xfe, buffer[7]); + EXPECT_EQ(0xff, buffer[8]); + + // Read back and verify + auto view = MakeUInt128SizesView(buffer, sizeof buffer); + EXPECT_EQ(nine_value, view.nine_byte().Read()); +} + +TEST(UInt128SizesView, CanReadMaxValues) { + ::std::uint8_t buffer[100]; + auto writer = UInt128SizesWriter(buffer, sizeof buffer); + + // Max 72-bit value + __uint128_t max_72 = (static_cast<__uint128_t>(1) << 72) - 1; + writer.nine_byte().Write(max_72); + EXPECT_EQ(max_72, + MakeUInt128SizesView(buffer, sizeof buffer).nine_byte().Read()); + + // Max 128-bit value + __uint128_t max_128 = + (static_cast<__uint128_t>(0xffffffffffffffffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + writer.sixteen_byte().Write(max_128); + EXPECT_EQ(max_128, + MakeUInt128SizesView(buffer, sizeof buffer).sixteen_byte().Read()); +} + +// A value that is out of range for a field wider than 64 bits (but narrower +// than 128) must be rejected by TryToWrite/CouldWriteValue rather than being +// silently truncated to the field width. This exercises the range check on +// the full 128-bit value. +TEST(UInt128SizesWriter, RejectsOutOfRangeValues) { + ::std::uint8_t buffer[100] = {}; + auto writer = UInt128SizesWriter(buffer, sizeof buffer); + + // nine_byte is 72 bits. 2**72 and 2**80 are out of range; their low 64 bits + // are zero, so a 64-bit-truncating check would wrongly accept them. + EXPECT_FALSE(writer.nine_byte().CouldWriteValue(static_cast<__uint128_t>(1) + << 72)); + EXPECT_FALSE(writer.nine_byte().TryToWrite(static_cast<__uint128_t>(1) << 72)); + EXPECT_FALSE(writer.nine_byte().TryToWrite(static_cast<__uint128_t>(1) << 80)); + + // The maximum in-range value still writes successfully. + __uint128_t max_72 = (static_cast<__uint128_t>(1) << 72) - 1; + EXPECT_TRUE(writer.nine_byte().TryToWrite(max_72)); + EXPECT_EQ(max_72, writer.nine_byte().Read()); +} + +TEST(Int128SizesWriter, RejectsOutOfRangeValues) { + ::std::uint8_t buffer[100] = {}; + auto writer = Int128SizesWriter(buffer, sizeof buffer); + + // nine_byte is 72 bits; range is [-2**71, 2**71). + __int128_t max_72 = (static_cast<__int128_t>(1) << 71) - 1; + __int128_t min_72 = -(static_cast<__int128_t>(1) << 71); + EXPECT_FALSE(writer.nine_byte().TryToWrite(max_72 + 1)); + EXPECT_FALSE(writer.nine_byte().TryToWrite(min_72 - 1)); + // Far below range, low 64 bits zero: must still be rejected. + EXPECT_FALSE( + writer.nine_byte().TryToWrite(-(static_cast<__int128_t>(1) << 100))); + + EXPECT_TRUE(writer.nine_byte().TryToWrite(min_72)); + EXPECT_EQ(min_72, writer.nine_byte().Read()); +} + +// Int128 tests with negative values +alignas(16) static const ::std::uint8_t kInt128SizesNegativeOne[100] = { + // All bytes are 0xff, representing -1 in two's complement for all sizes + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // nine_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // ten_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, // eleven_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, // twelve_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, // thirteen_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, // fourteen_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, // fifteen_byte + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, // sixteen_byte +}; + +TEST(Int128SizesView, CanReadNegativeOne) { + auto view = MakeInt128SizesView(kInt128SizesNegativeOne, + sizeof kInt128SizesNegativeOne); + EXPECT_TRUE(view.Ok()); + + EXPECT_EQ(static_cast<__int128_t>(-1), view.nine_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.ten_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.eleven_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.twelve_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.thirteen_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.fourteen_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.fifteen_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.sixteen_byte().Read()); +} + +TEST(Int128SizesWriter, CanWriteNegativeOne) { + ::std::uint8_t buffer[sizeof kInt128SizesNegativeOne]; + auto writer = Int128SizesWriter(buffer, sizeof buffer); + + writer.nine_byte().Write(static_cast<__int128_t>(-1)); + writer.ten_byte().Write(static_cast<__int128_t>(-1)); + writer.eleven_byte().Write(static_cast<__int128_t>(-1)); + writer.twelve_byte().Write(static_cast<__int128_t>(-1)); + writer.thirteen_byte().Write(static_cast<__int128_t>(-1)); + writer.fourteen_byte().Write(static_cast<__int128_t>(-1)); + writer.fifteen_byte().Write(static_cast<__int128_t>(-1)); + writer.sixteen_byte().Write(static_cast<__int128_t>(-1)); + + EXPECT_EQ(::std::vector( + kInt128SizesNegativeOne, + kInt128SizesNegativeOne + sizeof kInt128SizesNegativeOne), + ::std::vector(buffer, buffer + sizeof buffer)); +} + +TEST(Int128SizesView, CanReadMinMaxValues) { + ::std::uint8_t buffer[100]; + auto writer = Int128SizesWriter(buffer, sizeof buffer); + + // Test 72-bit signed min/max + __int128_t max_72 = (static_cast<__int128_t>(1) << 71) - 1; + __int128_t min_72 = -(static_cast<__int128_t>(1) << 71); + writer.nine_byte().Write(max_72); + EXPECT_EQ(max_72, + MakeInt128SizesView(buffer, sizeof buffer).nine_byte().Read()); + writer.nine_byte().Write(min_72); + EXPECT_EQ(min_72, + MakeInt128SizesView(buffer, sizeof buffer).nine_byte().Read()); + + // Test 128-bit signed min/max + __int128_t max_128 = static_cast<__int128_t>( + (static_cast<__uint128_t>(0x7fffffffffffffffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL)); + __int128_t min_128 = static_cast<__int128_t>(1) << 127; + + writer.sixteen_byte().Write(max_128); + EXPECT_EQ( + max_128, + MakeInt128SizesView(buffer, sizeof buffer).sixteen_byte().Read()); + writer.sixteen_byte().Write(min_128); + EXPECT_EQ( + min_128, + MakeInt128SizesView(buffer, sizeof buffer).sixteen_byte().Read()); +} + +// Test big-endian 128-bit structures +TEST(BigEndianUInt128SizesView, CanReadAndWrite) { + ::std::uint8_t buffer[100] = {}; + auto writer = BigEndianUInt128SizesWriter(buffer, sizeof buffer); + + // Write a recognizable value to nine_byte (72 bits) + __uint128_t nine_value = + (static_cast<__uint128_t>(0x01UL) << 64) | + static_cast<__uint128_t>(0x0203040506070809UL); + writer.nine_byte().Write(nine_value); + + // The bytes should be in big-endian order + EXPECT_EQ(0x01, buffer[0]); + EXPECT_EQ(0x02, buffer[1]); + EXPECT_EQ(0x09, buffer[8]); + + // Read back and verify + auto view = MakeBigEndianUInt128SizesView(buffer, sizeof buffer); + EXPECT_EQ(nine_value, view.nine_byte().Read()); +} + +TEST(BigEndianInt128SizesView, CanReadNegativeOne) { + ::std::uint8_t buffer[100]; + ::std::memset(buffer, 0xff, sizeof buffer); + + auto view = MakeBigEndianInt128SizesView(buffer, sizeof buffer); + EXPECT_EQ(static_cast<__int128_t>(-1), view.nine_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-1), view.sixteen_byte().Read()); +} + +// Test arrays of 128-bit values +TEST(UInt128ArraySizesView, CanReadAndWriteArrays) { + ::std::uint8_t buffer[200] = {}; + auto writer = UInt128ArraySizesWriter(buffer, sizeof buffer); + + // Write to first element of nine_byte array (72-bit elements) + __uint128_t value_0 = + (static_cast<__uint128_t>(0xffUL) << 64) | + static_cast<__uint128_t>(0x0102030405060708UL); + writer.nine_byte()[0].Write(value_0); + + __uint128_t value_1 = + (static_cast<__uint128_t>(0xeeUL) << 64) | + static_cast<__uint128_t>(0x1112131415161718UL); + writer.nine_byte()[1].Write(value_1); + + auto view = MakeUInt128ArraySizesView(buffer, sizeof buffer); + EXPECT_EQ(value_0, view.nine_byte()[0].Read()); + EXPECT_EQ(value_1, view.nine_byte()[1].Read()); + + // Test sixteen_byte array (128-bit elements) + __uint128_t sixteen_0 = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + __uint128_t sixteen_1 = + (static_cast<__uint128_t>(0x0123456789abcdefUL) << 64) | + static_cast<__uint128_t>(0xfedcba9876543210UL); + writer.sixteen_byte()[0].Write(sixteen_0); + writer.sixteen_byte()[1].Write(sixteen_1); + + EXPECT_EQ(sixteen_0, view.sixteen_byte()[0].Read()); + EXPECT_EQ(sixteen_1, view.sixteen_byte()[1].Read()); +} + +TEST(UInt128SizesView, CopyFrom) { + ::std::uint8_t buf_x[sizeof kUInt128Sizes] = {}; + ::std::uint8_t buf_y[sizeof kUInt128Sizes] = {}; + + auto x = UInt128SizesWriter(buf_x, sizeof buf_x); + auto y = UInt128SizesWriter(buf_y, sizeof buf_y); + + __uint128_t value = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + x.sixteen_byte().Write(value); + EXPECT_NE(x.sixteen_byte().Read(), y.sixteen_byte().Read()); + y.sixteen_byte().CopyFrom(x.sixteen_byte()); + EXPECT_EQ(x.sixteen_byte().Read(), y.sixteen_byte().Read()); +} + +TEST(Int128SizesView, CopyFrom) { + ::std::uint8_t buf_x[sizeof kInt128SizesNegativeOne] = {}; + ::std::uint8_t buf_y[sizeof kInt128SizesNegativeOne] = {}; + + auto x = Int128SizesWriter(buf_x, sizeof buf_x); + auto y = Int128SizesWriter(buf_y, sizeof buf_y); + + // Use a value that fits in int64_t to avoid literal issues + __int128_t large_negative = static_cast<__int128_t>(-1234567890123456789LL); + x.sixteen_byte().Write(large_negative); + EXPECT_NE(x.sixteen_byte().Read(), y.sixteen_byte().Read()); + y.sixteen_byte().CopyFrom(x.sixteen_byte()); + EXPECT_EQ(x.sixteen_byte().Read(), y.sixteen_byte().Read()); +} + +TEST(UInt128SizesView, TryToCopyFrom) { + ::std::uint8_t buf_x[sizeof kUInt128Sizes] = {}; + ::std::uint8_t buf_y[sizeof kUInt128Sizes] = {}; + + auto x = UInt128SizesWriter(buf_x, sizeof buf_x); + auto y = UInt128SizesWriter(buf_y, sizeof buf_y); + + __uint128_t value = + (static_cast<__uint128_t>(0xabcdef0123456789UL) << 64) | + static_cast<__uint128_t>(0x9876543210fedcbaUL); + x.sixteen_byte().Write(value); + EXPECT_NE(x.sixteen_byte().Read(), y.sixteen_byte().Read()); + EXPECT_TRUE(y.sixteen_byte().TryToCopyFrom(x.sixteen_byte())); + EXPECT_EQ(x.sixteen_byte().Read(), y.sixteen_byte().Read()); +} + +// Text format must round-trip values that do not fit in 64 bits, in both +// directions. Parsing a negative value into a >64-bit signed field in +// particular exercises the signedness check in DecodeInteger. +TEST(UInt128SizesView, TextFormatRoundTrip) { + ::std::uint8_t buffer[100] = {}; + auto writer = UInt128SizesWriter(buffer, sizeof buffer); + + // A 128-bit value that does not fit in 64 bits. + EXPECT_TRUE(::emboss::UpdateFromText( + writer, "{sixteen_byte: 0xfedcba98765432100123456789abcdef}")); + __uint128_t expected = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + EXPECT_EQ(expected, writer.sixteen_byte().Read()); + + // Out-of-range text for a 72-bit field must be rejected, not crash. + EXPECT_FALSE(::emboss::UpdateFromText( + writer, "{nine_byte: 0x1000000000000000000}")); // 2**72 +} + +TEST(Int128SizesView, TextFormatRoundTripNegative) { + ::std::uint8_t buffer[100] = {}; + auto writer = Int128SizesWriter(buffer, sizeof buffer); + + // Negative value into a 72-bit signed field: the '-' must be honored even + // though ::std::is_signed<__int128_t> is false under a strict -std. + EXPECT_TRUE(::emboss::UpdateFromText(writer, "{nine_byte: -12345}")); + EXPECT_EQ(static_cast<__int128_t>(-12345), writer.nine_byte().Read()); + + // A large negative 128-bit value, -(2**120), does not fit in 64 bits. (The + // WriteToString round-trip below covers the equivalent decimal text, + // -1329227995784915872903807060280344576.) + EXPECT_TRUE(::emboss::UpdateFromText( + writer, "{sixteen_byte: -0x1000000000000000000000000000000}")); + EXPECT_EQ(-(static_cast<__int128_t>(1) << 120), writer.sixteen_byte().Read()); + + // Round-trip through WriteToString and back. + ::std::string text = ::emboss::WriteToString( + MakeInt128SizesView(buffer, sizeof buffer)); + ::std::uint8_t buffer2[100] = {}; + auto writer2 = Int128SizesWriter(buffer2, sizeof buffer2); + EXPECT_TRUE(::emboss::UpdateFromText(writer2, text)); + EXPECT_EQ(-(static_cast<__int128_t>(1) << 120), + writer2.sixteen_byte().Read()); + EXPECT_EQ(static_cast<__int128_t>(-12345), writer2.nine_byte().Read()); +} + +} // namespace +} // namespace test +} // namespace emboss + +#else // !EMBOSS_HAS_INT128 + +// Provide a placeholder test when 128-bit integers are not available +namespace emboss { +namespace test { +namespace { + +TEST(Int128Support, NotAvailable) { + // This test exists just to indicate that 128-bit tests were skipped + // because the platform doesn't support __int128_t/__uint128_t. + GTEST_SKIP() << "128-bit integer support is not available on this platform"; +} + +} // namespace +} // namespace test +} // namespace emboss + +#endif // EMBOSS_HAS_INT128 diff --git a/compiler/front_end/constraints_test.py b/compiler/front_end/constraints_test.py index fb790a90..16385cbb 100644 --- a/compiler/front_end/constraints_test.py +++ b/compiler/front_end/constraints_test.py @@ -235,9 +235,10 @@ def test_struct_field_too_big_for_type(self): ) def test_bits_field_too_big_for_type(self): + # UInt supports up to 128 bits, so test with 129 bits (17 bytes) ir = _make_ir_from_emb( "struct Foo:\n" - " 0 [+9] UInt uint72\n" + " 0 [+17] UInt uint136\n" ' [byte_order: "LittleEndian"]\n' ) error_field = ir.module[0].type[0].structure.field[0] @@ -467,9 +468,11 @@ def test_explicit_size_too_small_for_field(self): ) def test_explicit_size_too_big(self): + # UInt supports up to 128 bits, so test with 256 bits (32 bytes) + # to ensure we get the "Requirements of UInt not met" error ir = _make_ir_from_emb( "struct Foo:\n" - " 0 [+16] UInt:128 one_twenty_eight_bit\n" + " 0 [+32] UInt:256 two_fifty_six_bit\n" ' [byte_order: "LittleEndian"]\n' ) error_field = ir.module[0].type[0].structure.field[0] diff --git a/compiler/front_end/prelude.emb b/compiler/front_end/prelude.emb index 5a54251c..53c83feb 100644 --- a/compiler/front_end/prelude.emb +++ b/compiler/front_end/prelude.emb @@ -25,14 +25,24 @@ external UInt: -- UInt is an automatically-sized unsigned integer. - [static_requirements: $is_statically_sized && 1 <= $static_size_in_bits <= 64] + -- + -- Note: Sizes above 64 bits (up to 128 bits) require platform support for + -- 128-bit integers (__uint128_t). The C++ backend will use EMBOSS_HAS_INT128 + -- to detect availability. On platforms without 128-bit integer support, + -- fields larger than 64 bits will fail to compile. + [static_requirements: $is_statically_sized && 1 <= $static_size_in_bits <= 128] [is_integer: true] [addressable_unit_size: 1] external Int: -- Int is an automatically-sized signed 2's-complement integer. - [static_requirements: $is_statically_sized && 1 <= $static_size_in_bits <= 64] + -- + -- Note: Sizes above 64 bits (up to 128 bits) require platform support for + -- 128-bit integers (__int128_t). The C++ backend will use EMBOSS_HAS_INT128 + -- to detect availability. On platforms without 128-bit integer support, + -- fields larger than 64 bits will fail to compile. + [static_requirements: $is_statically_sized && 1 <= $static_size_in_bits <= 128] [is_integer: true] [addressable_unit_size: 1] diff --git a/doc/language-reference.md b/doc/language-reference.md index db8682e2..5b28e409 100644 --- a/doc/language-reference.md +++ b/doc/language-reference.md @@ -873,6 +873,10 @@ structured view of bits. Fields in a `bits` must have bit-oriented types (such as other `bits`, `UInt`, `Bcd`, `Flag`). Byte-oriented types, such as `struct`s, may not be embedded in a `bits`. +A `bits` must be fixed size, and may be no more than 64 bits in total size. +Note that this limit is smaller than the 128-bit maximum size of a `UInt` or +`Int` field in a `struct`. + For example: ``` @@ -950,6 +954,11 @@ struct Message: In this case, the fields of the `bits` will be treated as though they are fields of the outer struct. +Like any other `bits`, an anonymous `bits` is limited to 64 bits in total size, +even though the enclosing `struct` may contain integer fields of up to 128 +bits. Supporting larger `bits` would require additional implementation work in +the compiler and runtime. + #### Inline `bits` @@ -1078,17 +1087,29 @@ the Prelude documentation from prelude.emb. --> ### `UInt` -A `UInt` is an unsigned integer. `UInt` can be anywhere from 1 to 64 bits in +A `UInt` is an unsigned integer. `UInt` can be anywhere from 1 to 128 bits in size, and may be used both in `struct`s and in `bits`. `UInt` fields may be referenced in integer expressions. +`UInt`s larger than 64 bits require 128-bit integer support from the target +platform: for the C++ backend, a compiler that provides `__uint128_t` (detected +via `EMBOSS_HAS_INT128`). On platforms without 128-bit integer support, +generated code for fields larger than 64 bits will fail to compile. Since +`bits` types are limited to 64 bits in total size, `UInt`s larger than 64 bits +can only be used in `struct`s. `UInt` fields larger than 64 bits cannot be +referenced in integer expressions. + ### `Int` An `Int` is a signed two's-complement integer. `Int` can be anywhere from 1 to -64 bits in size, and may be used both in `struct`s and in `bits`. `Int` fields +128 bits in size, and may be used both in `struct`s and in `bits`. `Int` fields may be referenced in integer expressions. +Like `UInt`, `Int`s larger than 64 bits require 128-bit integer support from +the target platform (for the C++ backend, `__int128_t`), can only be used in +`struct`s, and cannot be referenced in integer expressions. + ### `Bcd` diff --git a/runtime/cpp/emboss_bit_util.h b/runtime/cpp/emboss_bit_util.h index 3e93f96a..f9b031c5 100644 --- a/runtime/cpp/emboss_bit_util.h +++ b/runtime/cpp/emboss_bit_util.h @@ -56,6 +56,14 @@ inline constexpr ::std::uint64_t ByteSwap(::std::uint64_t x) { #endif } +#if EMBOSS_HAS_INT128 +inline constexpr EMBOSS_UINT128_T ByteSwap(EMBOSS_UINT128_T x) { + return (static_cast(ByteSwap(static_cast<::std::uint64_t>(x))) + << 64) | + ByteSwap(static_cast<::std::uint64_t>(x >> 64)); +} +#endif // EMBOSS_HAS_INT128 + // Masks the given value to the given number of bits. template inline constexpr T MaskToNBits(T value, unsigned bits) { diff --git a/runtime/cpp/emboss_cpp_types.h b/runtime/cpp/emboss_cpp_types.h index 9a1c2117..a9dd9f7c 100644 --- a/runtime/cpp/emboss_cpp_types.h +++ b/runtime/cpp/emboss_cpp_types.h @@ -21,6 +21,8 @@ #include #include +#include "runtime/cpp/emboss_defines.h" + namespace emboss { namespace support { @@ -51,12 +53,36 @@ struct FloatType<32> final { // LeastWidthInteger::Unsigned is the smallest uintNN_t type that can // hold n_bits or more. LeastWidthInteger::Signed is the corresponding // signed type. +// +// The exact-width specializations below always take precedence, so the +// unspecialized template is only ever instantiated for widths with no +// exact-width integer type. It recursively "rounds up" such widths to the +// next supported width: for example, LeastWidthInteger<12>::Unsigned +// instantiates LeastWidthInteger<13> through LeastWidthInteger<16>, yielding +// ::std::uint16_t. The static_assert bounds the recursion for widths above +// the maximum supported width. +#if EMBOSS_HAS_INT128 +template +struct LeastWidthInteger final { + static_assert(kBits <= 128, "Only bit sizes up to 128 are supported."); + using Unsigned = typename LeastWidthInteger::Unsigned; + using Signed = typename LeastWidthInteger::Signed; +}; +template <> +struct LeastWidthInteger<128> final { + using Unsigned = EMBOSS_UINT128_T; + using Signed = EMBOSS_INT128_T; +}; +#else template struct LeastWidthInteger final { - static_assert(kBits <= 64, "Only bit sizes up to 64 are supported."); + static_assert(kBits <= 64, + "Bit widths over 64 require 128-bit integer support, which is " + "not available on this platform (EMBOSS_HAS_INT128 is 0)."); using Unsigned = typename LeastWidthInteger::Unsigned; using Signed = typename LeastWidthInteger::Signed; }; +#endif // EMBOSS_HAS_INT128 template <> struct LeastWidthInteger<64> final { using Unsigned = ::std::uint64_t; @@ -78,6 +104,19 @@ struct LeastWidthInteger<8> final { using Signed = ::std::int8_t; }; +// MaxWidthUnsigned and MaxWidthSigned are the widest unsigned and signed +// integer types that Emboss supports on the current platform. They are used +// when a value of unknown (but supported) width must be compared without +// truncation -- for example, when range-checking a value before writing it to +// an integer field. +#if EMBOSS_HAS_INT128 +using MaxWidthUnsigned = EMBOSS_UINT128_T; +using MaxWidthSigned = EMBOSS_INT128_T; +#else +using MaxWidthUnsigned = ::std::uint64_t; +using MaxWidthSigned = ::std::int64_t; +#endif // EMBOSS_HAS_INT128 + // IsAliasSafe::value is true if T is an alias safe type; i.e. const? // volatile? (unsigned)? char | std::byte. template diff --git a/runtime/cpp/emboss_defines.h b/runtime/cpp/emboss_defines.h index 24b5af06..21f49667 100644 --- a/runtime/cpp/emboss_defines.h +++ b/runtime/cpp/emboss_defines.h @@ -311,4 +311,24 @@ #define EMBOSS_SYSTEM_IS_TWOS_COMPLEMENT 0 #endif // !defined(EMBOSS_SYSTEM_IS_TWOS_COMPLEMENT) +// Detect 128-bit integer support. +// __uint128_t and __int128_t are available on GCC and Clang on 64-bit targets. +#if !defined(EMBOSS_HAS_INT128) +#if defined(__SIZEOF_INT128__) +#define EMBOSS_HAS_INT128 1 +#else +#define EMBOSS_HAS_INT128 0 +#endif // defined(__SIZEOF_INT128__) +#endif // !defined(EMBOSS_HAS_INT128) + +#if EMBOSS_HAS_INT128 +#if !defined(EMBOSS_INT128_T) +#define EMBOSS_INT128_T __int128_t +#endif // !defined(EMBOSS_INT128_T) + +#if !defined(EMBOSS_UINT128_T) +#define EMBOSS_UINT128_T __uint128_t +#endif // !defined(EMBOSS_UINT128_T) +#endif // EMBOSS_HAS_INT128 + #endif // EMBOSS_RUNTIME_CPP_EMBOSS_DEFINES_H_ diff --git a/runtime/cpp/emboss_memory_util.h b/runtime/cpp/emboss_memory_util.h index fca71c9b..77e6cb50 100644 --- a/runtime/cpp/emboss_memory_util.h +++ b/runtime/cpp/emboss_memory_util.h @@ -991,8 +991,13 @@ template class BitBlock final { static_assert(kBufferSizeInBits % 8 == 0, "BitBlock can only operate on byte buffers."); +#if EMBOSS_HAS_INT128 + static_assert(kBufferSizeInBits <= 128, + "BitBlock can only operate on buffers up to 128 bits."); +#else static_assert(kBufferSizeInBits <= 64, "BitBlock can only operate on small buffers."); +#endif // EMBOSS_HAS_INT128 public: using ValueType = typename LeastWidthInteger::Unsigned; diff --git a/runtime/cpp/emboss_prelude.h b/runtime/cpp/emboss_prelude.h index ad302cdd..9acd2f2e 100644 --- a/runtime/cpp/emboss_prelude.h +++ b/runtime/cpp/emboss_prelude.h @@ -264,7 +264,7 @@ class UIntView final { // where N is the number of bits in the type, this is entirely // standards-compliant. return value >= 0 && - static_cast(value) <= + static_cast<::emboss::support::MaxWidthUnsigned>(value) <= ((static_cast(1) << (Parameters::kBits - 1)) << 1) - 1 && Parameters::ValueIsOk(static_cast(value)); @@ -416,9 +416,16 @@ class IntView final { // ((1 << 6) - 1) * 2 + 1, which is (64 - 1) * 2 + 1, which is 63 * 2 + 1, // which is 126 + 1, which is 127. The upper bound must be computed in // multiple steps like this in order to avoid overflow. - return (!::std::is_signed::type>::type>::value || - static_cast(value) >= + // Note: signedness is checked via ::std::numeric_limits<>::is_signed rather + // than ::std::is_signed<>, because ::std::is_signed is false for extended + // integer types like __int128_t under a strict -std=c++NN (they are not + // "integral types" in the standard sense), whereas numeric_limits *is* + // specialized for them. This must match the is_integer check used to + // enable this method, above. + return (!::std::numeric_limits::type>::type>:: + is_signed || + static_cast<::emboss::support::MaxWidthSigned>(value) >= (Parameters::kBits == 1 ? -1 : (static_cast(1) << (Parameters::kBits - 2)) * diff --git a/runtime/cpp/emboss_text_util.h b/runtime/cpp/emboss_text_util.h index 9cfe03d3..061ea317 100644 --- a/runtime/cpp/emboss_text_util.h +++ b/runtime/cpp/emboss_text_util.h @@ -132,7 +132,11 @@ bool DecodeInteger(const ::std::string &text, IntType *result) { IntType base = 10; bool negative = false; unsigned offset = 0; - if (::std::is_signed::value && text.size() >= 1 + offset && + // ::std::numeric_limits<>::is_signed is used rather than ::std::is_signed<> + // because the latter is false for extended integer types such as __int128_t + // under a strict -std=c++NN, which would prevent parsing negative values into + // those types. numeric_limits *is* specialized for them. + if (::std::numeric_limits::is_signed && text.size() >= 1 + offset && text[offset] == '-') { negative = true; offset += 1; diff --git a/runtime/cpp/test/emboss_bit_util_test.cc b/runtime/cpp/test/emboss_bit_util_test.cc index 52bff49d..0592bf31 100644 --- a/runtime/cpp/test/emboss_bit_util_test.cc +++ b/runtime/cpp/test/emboss_bit_util_test.cc @@ -26,6 +26,17 @@ TEST(ByteSwap, ByteSwap) { EXPECT_EQ(0x01020304U, ByteSwap(::std::uint32_t{0x04030201})); EXPECT_EQ(0x0102030405060708UL, ByteSwap(::std::uint64_t{0x0807060504030201UL})); +#if EMBOSS_HAS_INT128 + // Create 128-bit value 0x0f0e0d0c0b0a09080706050403020100 + __uint128_t val128 = + (static_cast<__uint128_t>(0x0807060504030201UL) << 64) | + static_cast<__uint128_t>(0x100f0e0d0c0b0a09UL); + // Expected result after swap: 0x090a0b0c0d0e0f10_0102030405060708 + __uint128_t expected128 = + (static_cast<__uint128_t>(0x090a0b0c0d0e0f10UL) << 64) | + static_cast<__uint128_t>(0x0102030405060708UL); + EXPECT_EQ(expected128, ByteSwap(val128)); +#endif // EMBOSS_HAS_INT128 } TEST(MaskToNBits, MaskToNBits) { @@ -36,6 +47,22 @@ TEST(MaskToNBits, MaskToNBits) { EXPECT_EQ(0xffffffffU, MaskToNBits(0xffffffffU, 32)); EXPECT_EQ(0xffffffffffffffffU, MaskToNBits(0xffffffffffffffffU, 64)); EXPECT_EQ(0xfU, MaskToNBits(::std::uint8_t{0xff}, 4)); +#if EMBOSS_HAS_INT128 + // Create a 128-bit value with all bits set + __uint128_t all_ones_128 = + (static_cast<__uint128_t>(0xffffffffffffffffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + // Mask to 72 bits + __uint128_t expected_72 = + (static_cast<__uint128_t>(0xffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + EXPECT_EQ(expected_72, MaskToNBits(all_ones_128, 72)); + // Mask to 128 bits should return the original value + EXPECT_EQ(all_ones_128, MaskToNBits(all_ones_128, 128)); + // Mask to 64 bits should return just the lower 64 bits + __uint128_t expected_64 = static_cast<__uint128_t>(0xffffffffffffffffUL); + EXPECT_EQ(expected_64, MaskToNBits(all_ones_128, 64)); +#endif // EMBOSS_HAS_INT128 } TEST(IsPowerOfTwo, IsPowerOfTwo) { @@ -71,6 +98,31 @@ TEST(IsPowerOfTwo, IsPowerOfTwo) { EXPECT_FALSE(IsPowerOfTwo(::std::int8_t{-128})); EXPECT_FALSE(IsPowerOfTwo(::std::int8_t{65})); EXPECT_FALSE(IsPowerOfTwo(::std::int8_t{127})); +#if EMBOSS_HAS_INT128 + // Test powers of two for 128-bit values + __uint128_t one_128 = 1; + EXPECT_TRUE(IsPowerOfTwo(one_128)); + EXPECT_TRUE(IsPowerOfTwo(one_128 << 64)); + EXPECT_TRUE(IsPowerOfTwo(one_128 << 100)); + EXPECT_TRUE(IsPowerOfTwo(one_128 << 127)); + EXPECT_FALSE(IsPowerOfTwo(static_cast<__uint128_t>(0))); + EXPECT_FALSE(IsPowerOfTwo(static_cast<__uint128_t>(3))); + EXPECT_FALSE(IsPowerOfTwo((one_128 << 127) - 1)); + EXPECT_FALSE(IsPowerOfTwo((one_128 << 64) + 1)); + + // Test signed 128-bit values + __int128_t signed_one_128 = 1; + EXPECT_TRUE(IsPowerOfTwo(signed_one_128)); + EXPECT_TRUE(IsPowerOfTwo(signed_one_128 << 64)); + EXPECT_TRUE(IsPowerOfTwo(signed_one_128 << 100)); + EXPECT_TRUE(IsPowerOfTwo(signed_one_128 << 126)); // Max positive power of 2 + EXPECT_FALSE(IsPowerOfTwo(static_cast<__int128_t>(0))); + EXPECT_FALSE(IsPowerOfTwo(static_cast<__int128_t>(-1))); + EXPECT_FALSE(IsPowerOfTwo(static_cast<__int128_t>(-2))); + // Min int128 is negative, so should not be a power of two + __int128_t min_int128 = static_cast<__int128_t>(1) << 127; + EXPECT_FALSE(IsPowerOfTwo(min_int128)); +#endif // EMBOSS_HAS_INT128 } #if defined(EMBOSS_LITTLE_ENDIAN_TO_NATIVE) @@ -181,6 +233,71 @@ TEST(EndianConversion, NativeToBigEndian) { } #endif // defined(EMBOSS_NATIVE_TO_BIG_ENDIAN) +#if EMBOSS_HAS_INT128 +TEST(ByteSwap, ByteSwap128Comprehensive) { + // Test identity: swapping twice should return the original value + __uint128_t original = + (static_cast<__uint128_t>(0x0102030405060708UL) << 64) | + static_cast<__uint128_t>(0x090a0b0c0d0e0f10UL); + EXPECT_EQ(original, ByteSwap(ByteSwap(original))); + + // Test with value where high and low 64-bit halves are different + __uint128_t asymmetric = + (static_cast<__uint128_t>(0xFFFFFFFFFFFFFFFFUL) << 64) | + static_cast<__uint128_t>(0x0000000000000000UL); + __uint128_t asymmetric_swapped = + (static_cast<__uint128_t>(0x0000000000000000UL) << 64) | + static_cast<__uint128_t>(0xFFFFFFFFFFFFFFFFUL); + EXPECT_EQ(asymmetric_swapped, ByteSwap(asymmetric)); + + // Test with single byte set at position 0 + __uint128_t byte0 = static_cast<__uint128_t>(0x42UL); + __uint128_t byte0_swapped = static_cast<__uint128_t>(0x42UL) << 120; + EXPECT_EQ(byte0_swapped, ByteSwap(byte0)); + + // Test with single byte set at position 15 (highest byte) + __uint128_t byte15 = static_cast<__uint128_t>(0x42UL) << 120; + __uint128_t byte15_swapped = static_cast<__uint128_t>(0x42UL); + EXPECT_EQ(byte15_swapped, ByteSwap(byte15)); + + // Test with value where each byte is unique + __uint128_t unique_bytes = + (static_cast<__uint128_t>(0x0102030405060708UL) << 64) | + static_cast<__uint128_t>(0x090a0b0c0d0e0f10UL); + __uint128_t unique_bytes_swapped = + (static_cast<__uint128_t>(0x100f0e0d0c0b0a09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(unique_bytes_swapped, ByteSwap(unique_bytes)); +} + +TEST(MaskToNBits, MaskToNBits128EdgeCases) { + __uint128_t all_ones_128 = + (static_cast<__uint128_t>(0xffffffffffffffffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + + // Mask to 1 bit + EXPECT_EQ(static_cast<__uint128_t>(1), MaskToNBits(all_ones_128, 1)); + + // Mask to 65 bits (just over 64) + __uint128_t expected_65 = + (static_cast<__uint128_t>(0x1UL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + EXPECT_EQ(expected_65, MaskToNBits(all_ones_128, 65)); + + // Mask to 127 bits + __uint128_t expected_127 = + (static_cast<__uint128_t>(0x7fffffffffffffffUL) << 64) | + static_cast<__uint128_t>(0xffffffffffffffffUL); + EXPECT_EQ(expected_127, MaskToNBits(all_ones_128, 127)); + + // Mask value that already fits + __uint128_t small_value = static_cast<__uint128_t>(0x1234UL); + EXPECT_EQ(small_value, MaskToNBits(small_value, 128)); + EXPECT_EQ(small_value, MaskToNBits(small_value, 64)); + EXPECT_EQ(static_cast<__uint128_t>(0x234UL), MaskToNBits(small_value, 12)); +} +#endif // EMBOSS_HAS_INT128 + } // namespace test } // namespace support } // namespace emboss diff --git a/runtime/cpp/test/emboss_cpp_types_test.cc b/runtime/cpp/test/emboss_cpp_types_test.cc index e0d63083..41306b96 100644 --- a/runtime/cpp/test/emboss_cpp_types_test.cc +++ b/runtime/cpp/test/emboss_cpp_types_test.cc @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include - #include "runtime/cpp/emboss_cpp_types.h" +#include + #include "gtest/gtest.h" namespace emboss { @@ -100,6 +100,24 @@ TEST(LeastWidthInteger, Types) { ::std::is_same::Unsigned, ::std::uint64_t>::value)); EXPECT_TRUE( (::std::is_same::Signed, ::std::int64_t>::value)); +#if EMBOSS_HAS_INT128 + EXPECT_TRUE( + (::std::is_same::Unsigned, __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Signed, __int128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Unsigned, __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Signed, __int128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Unsigned, __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Signed, __int128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Unsigned, __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same::Signed, __int128_t>::value)); +#endif // EMBOSS_HAS_INT128 } TEST(IsAliasSafe, CharTypes) { diff --git a/runtime/cpp/test/emboss_memory_util_test.cc b/runtime/cpp/test/emboss_memory_util_test.cc index 18310857..b67dc701 100644 --- a/runtime/cpp/test/emboss_memory_util_test.cc +++ b/runtime/cpp/test/emboss_memory_util_test.cc @@ -150,6 +150,118 @@ TEST(MemoryAccessor, LittleEndianReads) { TestMemoryAccessor(); } +#if EMBOSS_HAS_INT128 +// Test 128-bit MemoryAccessor operations +TEST(MemoryAccessor, Int128LittleEndianReadWrite) { + alignas(16) unsigned char bytes[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10}; + __uint128_t expected_le = + (static_cast<__uint128_t>(0x100f0e0d0c0b0a09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected_le, + (MemoryAccessor::ReadLittleEndianUInt( + bytes))); + + // Write and verify + __uint128_t write_value = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + MemoryAccessor::WriteLittleEndianUInt(bytes, + write_value); + EXPECT_EQ(0xef, bytes[0]); + EXPECT_EQ(0xcd, bytes[1]); + EXPECT_EQ(0xab, bytes[2]); + EXPECT_EQ(0x89, bytes[3]); + EXPECT_EQ(0x67, bytes[4]); + EXPECT_EQ(0x45, bytes[5]); + EXPECT_EQ(0x23, bytes[6]); + EXPECT_EQ(0x01, bytes[7]); + EXPECT_EQ(0x10, bytes[8]); + EXPECT_EQ(0x32, bytes[9]); + EXPECT_EQ(0x54, bytes[10]); + EXPECT_EQ(0x76, bytes[11]); + EXPECT_EQ(0x98, bytes[12]); + EXPECT_EQ(0xba, bytes[13]); + EXPECT_EQ(0xdc, bytes[14]); + EXPECT_EQ(0xfe, bytes[15]); +} + +TEST(MemoryAccessor, Int128BigEndianReadWrite) { + alignas(16) unsigned char bytes[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10}; + __uint128_t expected_be = + (static_cast<__uint128_t>(0x0102030405060708UL) << 64) | + static_cast<__uint128_t>(0x090a0b0c0d0e0f10UL); + EXPECT_EQ( + expected_be, + (MemoryAccessor::ReadBigEndianUInt(bytes))); + + // Write and verify + __uint128_t write_value = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + MemoryAccessor::WriteBigEndianUInt(bytes, + write_value); + EXPECT_EQ(0xfe, bytes[0]); + EXPECT_EQ(0xdc, bytes[1]); + EXPECT_EQ(0xba, bytes[2]); + EXPECT_EQ(0x98, bytes[3]); + EXPECT_EQ(0x76, bytes[4]); + EXPECT_EQ(0x54, bytes[5]); + EXPECT_EQ(0x32, bytes[6]); + EXPECT_EQ(0x10, bytes[7]); + EXPECT_EQ(0x01, bytes[8]); + EXPECT_EQ(0x23, bytes[9]); + EXPECT_EQ(0x45, bytes[10]); + EXPECT_EQ(0x67, bytes[11]); + EXPECT_EQ(0x89, bytes[12]); + EXPECT_EQ(0xab, bytes[13]); + EXPECT_EQ(0xcd, bytes[14]); + EXPECT_EQ(0xef, bytes[15]); +} + +TEST(MemoryAccessor, Int128NonFullWidth) { + // Test reading/writing values less than 128 bits but more than 64 + alignas(16) unsigned char bytes[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10}; + + // Read 72 bits (9 bytes) little-endian + __uint128_t expected_72_le = + (static_cast<__uint128_t>(0x09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected_72_le, + (MemoryAccessor::ReadLittleEndianUInt( + bytes))); + + // Read 72 bits (9 bytes) big-endian + __uint128_t expected_72_be = + (static_cast<__uint128_t>(0x01UL) << 64) | + static_cast<__uint128_t>(0x0203040506070809UL); + EXPECT_EQ( + expected_72_be, + (MemoryAccessor::ReadBigEndianUInt(bytes))); + + // Read 96 bits (12 bytes) little-endian + __uint128_t expected_96_le = + (static_cast<__uint128_t>(0x0c0b0a09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected_96_le, + (MemoryAccessor::ReadLittleEndianUInt( + bytes))); + + // Read 96 bits (12 bytes) big-endian + __uint128_t expected_96_be = + (static_cast<__uint128_t>(0x01020304UL) << 64) | + static_cast<__uint128_t>(0x05060708090a0b0cUL); + EXPECT_EQ( + expected_96_be, + (MemoryAccessor::ReadBigEndianUInt(bytes))); +} +#endif // EMBOSS_HAS_INT128 + TEST(ContiguousBuffer, OffsetStorageType) { EXPECT_TRUE((::std::is_same< ContiguousBuffer, @@ -675,6 +787,156 @@ TEST(BitBlock, GetOffsetStorage) { .SizeInBits())); } +#if EMBOSS_HAS_INT128 +template +using BigEndianBitBlockN128 = + BitBlock, kBits>; + +template +using LittleEndianBitBlockN128 = + BitBlock, kBits>; + +TEST(BitBlock, Int128BigEndianMethods) { + ::std::uint8_t bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + const auto big_endian = + BigEndianBitBlockN128<128>{ReadWriteContiguousBuffer{bytes, 16}}; + EXPECT_EQ(128U, big_endian.SizeInBits()); + EXPECT_TRUE(big_endian.Ok()); + + __uint128_t expected = + (static_cast<__uint128_t>(0x0102030405060708UL) << 64) | + static_cast<__uint128_t>(0x090a0b0c0d0e0f10UL); + EXPECT_EQ(expected, big_endian.ReadUInt()); + EXPECT_EQ(expected, big_endian.UncheckedReadUInt()); + EXPECT_FALSE(BigEndianBitBlockN128<128>().Ok()); + EXPECT_EQ(128U, BigEndianBitBlockN128<128>().SizeInBits()); +} + +TEST(BitBlock, Int128LittleEndianMethods) { + ::std::uint8_t bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + const auto little_endian = + LittleEndianBitBlockN128<128>{ReadWriteContiguousBuffer{bytes, 16}}; + EXPECT_EQ(128U, little_endian.SizeInBits()); + EXPECT_TRUE(little_endian.Ok()); + + __uint128_t expected = + (static_cast<__uint128_t>(0x100f0e0d0c0b0a09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected, little_endian.ReadUInt()); + EXPECT_EQ(expected, little_endian.UncheckedReadUInt()); + EXPECT_FALSE(LittleEndianBitBlockN128<128>().Ok()); + EXPECT_EQ(128U, LittleEndianBitBlockN128<128>().SizeInBits()); +} + +TEST(BitBlock, Int128WriteOperations) { + ::std::uint8_t bytes[16] = {}; + auto little_endian = + LittleEndianBitBlockN128<128>{ReadWriteContiguousBuffer{bytes, 16}}; + + __uint128_t write_value = + (static_cast<__uint128_t>(0xfedcba9876543210UL) << 64) | + static_cast<__uint128_t>(0x0123456789abcdefUL); + little_endian.WriteUInt(write_value); + + // Verify little-endian byte order + EXPECT_EQ(0xef, bytes[0]); + EXPECT_EQ(0xcd, bytes[1]); + EXPECT_EQ(0xab, bytes[2]); + EXPECT_EQ(0x89, bytes[3]); + EXPECT_EQ(0x67, bytes[4]); + EXPECT_EQ(0x45, bytes[5]); + EXPECT_EQ(0x23, bytes[6]); + EXPECT_EQ(0x01, bytes[7]); + EXPECT_EQ(0x10, bytes[8]); + EXPECT_EQ(0x32, bytes[9]); + EXPECT_EQ(0x54, bytes[10]); + EXPECT_EQ(0x76, bytes[11]); + EXPECT_EQ(0x98, bytes[12]); + EXPECT_EQ(0xba, bytes[13]); + EXPECT_EQ(0xdc, bytes[14]); + EXPECT_EQ(0xfe, bytes[15]); + + EXPECT_EQ(write_value, little_endian.ReadUInt()); +} + +TEST(BitBlock, Int128NonFullWidthSizes) { + // Test 72-bit (9-byte), 96-bit (12-byte), 104-bit (13-byte) values + ::std::uint8_t bytes[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + + // Test 72-bit little-endian + const auto le_72 = + LittleEndianBitBlockN128<72>{ReadWriteContiguousBuffer{bytes, 9}}; + EXPECT_TRUE(le_72.Ok()); + __uint128_t expected_72_le = + (static_cast<__uint128_t>(0x09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(expected_72_le, le_72.ReadUInt()); + + // Test 96-bit big-endian + const auto be_96 = + BigEndianBitBlockN128<96>{ReadWriteContiguousBuffer{bytes, 12}}; + EXPECT_TRUE(be_96.Ok()); + __uint128_t expected_96_be = + (static_cast<__uint128_t>(0x01020304UL) << 64) | + static_cast<__uint128_t>(0x05060708090a0b0cUL); + EXPECT_EQ(expected_96_be, be_96.ReadUInt()); +} + +TEST(BitBlock, Int128GetOffsetStorage) { + ::std::uint8_t bytes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + const auto bit_block = + LittleEndianBitBlockN128<128>{ReadWriteContiguousBuffer{bytes, 16}}; + + // Get offset storage for bits 64-127 (upper 64 bits) + const auto offset_block = bit_block.GetOffsetStorage<1, 0>(64, 64); + EXPECT_EQ(64U, offset_block.SizeInBits()); + EXPECT_TRUE(offset_block.Ok()); + EXPECT_EQ(0x100f0e0d0c0b0a09UL, offset_block.ReadUInt()); + + // Get offset storage for bits 0-63 (lower 64 bits) + const auto lower_block = bit_block.GetOffsetStorage<1, 0>(0, 64); + EXPECT_EQ(64U, lower_block.SizeInBits()); + EXPECT_TRUE(lower_block.Ok()); + EXPECT_EQ(0x0807060504030201UL, lower_block.ReadUInt()); + + // Get offset storage that spans the 64-bit boundary + const auto span_block = bit_block.GetOffsetStorage<1, 0>(32, 64); + EXPECT_EQ(64U, span_block.SizeInBits()); + EXPECT_TRUE(span_block.Ok()); + // Bits 32-95 from original value + __uint128_t full_value = + (static_cast<__uint128_t>(0x100f0e0d0c0b0a09UL) << 64) | + static_cast<__uint128_t>(0x0807060504030201UL); + EXPECT_EQ(static_cast<::std::uint64_t>((full_value >> 32) & 0xFFFFFFFFFFFFFFFFUL), + span_block.ReadUInt()); +} + +TEST(ContiguousBuffer, Int128ReturnType) { + const auto buffer = ContiguousBuffer(); + +#if EMBOSS_HAS_INT128 + EXPECT_TRUE((::std::is_same()), + __uint128_t>::value)); + EXPECT_TRUE((::std::is_same()), + __uint128_t>::value)); + EXPECT_TRUE((::std::is_same()), + __uint128_t>::value)); + EXPECT_TRUE((::std::is_same()), + __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same()), + __uint128_t>::value)); + EXPECT_TRUE( + (::std::is_same()), + __uint128_t>::value)); +#endif // EMBOSS_HAS_INT128 +} +#endif // EMBOSS_HAS_INT128 + TEST(OffsetBitBlock, Methods) { ::std::vector bytes = { {0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09}}; diff --git a/runtime/cpp/test/emboss_prelude_test.cc b/runtime/cpp/test/emboss_prelude_test.cc index 889daae7..e488f99a 100644 --- a/runtime/cpp/test/emboss_prelude_test.cc +++ b/runtime/cpp/test/emboss_prelude_test.cc @@ -38,9 +38,9 @@ using ViewParameters = ::emboss::support::FixedSizeViewParameters< TEST(FlagView, Methods) { ::std::uint8_t byte = 0; - auto flag_view = - FlagView, OffsetBitBlock>>{BitBlockN<8>{ - ReadWriteContiguousBuffer{&byte, 1}}.GetOffsetStorage<1, 0>(0, 1)}; + auto flag_view = FlagView, OffsetBitBlock>>{ + BitBlockN<8>{ReadWriteContiguousBuffer{&byte, 1}}.GetOffsetStorage<1, 0>( + 0, 1)}; EXPECT_FALSE(flag_view.Read()); byte = 0xfe; EXPECT_FALSE(flag_view.Read()); @@ -60,8 +60,9 @@ TEST(FlagView, Methods) { TEST(FlagView, TextDecode) { ::std::uint8_t byte = 0; const auto flag_view = - FlagView, OffsetBitBlock>>{BitBlockN<8>{ - ReadWriteContiguousBuffer{&byte, 1}}.GetOffsetStorage<1, 0>(0, 1)}; + FlagView, OffsetBitBlock>>{ + BitBlockN<8>{ReadWriteContiguousBuffer{&byte, 1}} + .GetOffsetStorage<1, 0>(0, 1)}; EXPECT_FALSE(UpdateFromText(flag_view, "")); EXPECT_FALSE(UpdateFromText(flag_view, "FALSE")); EXPECT_FALSE(UpdateFromText(flag_view, "TRUE")); @@ -85,17 +86,31 @@ TEST(FlagView, TextDecode) { TEST(FlagView, TextEncode) { ::std::uint8_t byte = 0; const auto flag_view = - FlagView, OffsetBitBlock>>{BitBlockN<8>{ - ReadWriteContiguousBuffer{&byte, 1}}.GetOffsetStorage<1, 0>(0, 1)}; + FlagView, OffsetBitBlock>>{ + BitBlockN<8>{ReadWriteContiguousBuffer{&byte, 1}} + .GetOffsetStorage<1, 0>(0, 1)}; EXPECT_EQ("false", WriteToString(flag_view)); byte = 1; EXPECT_EQ("true", WriteToString(flag_view)); } +// Helper to select appropriate BitBlock size for a given view size +template +struct BitBlockSelector { +#if EMBOSS_HAS_INT128 + // Use 128-bit BitBlock for sizes > 64 + using Type = typename ::std::conditional<(kBits > 64), BitBlockN<128>, + BitBlockN<64>>::type; +#else + using Type = BitBlockN<64>; +#endif +}; + template