// Value encoding: map PLC values (coils, 16/32-bit integers) onto the raw
// 16-bit words that travel on the Modbus wire, honouring byte order and word
// order. This is the layer a driver uses to translate a point-table register
// into the bytes sent to / received from the slave.
//
// Conventions
// -----------
// A "word" is a 16-bit value stored as an `Int` in 0..=65535.
// Byte order says which of a word's two bytes is transmitted first.
// Word order says which of a 32-bit value's two words occupies the lower
// register address.
//
// All arithmetic stays inside the 32-bit `Int` range, so the code behaves
// identically on every backend without depending on 64-bit-wide globals.

///|
/// How the two bytes of a 16-bit word are placed on the wire.
pub enum ByteOrder {
  /// Big endian: high byte transmitted first.
  BigEndian
  /// Little endian: low byte transmitted first.
  LittleEndian
}

///|
/// In which register the high word of a 32-bit value is mapped.
pub enum WordOrder {
  /// Native order: the high word occupies the lower register address.
  HighWordLowAddress
  /// Byte-swapped order: the low word occupies the lower register address.
  LowWordLowAddress
}

///|
/// The same value laid out according to the reverse of an order.
fn swap_byte_order(order : ByteOrder) -> ByteOrder {
  match order {
    BigEndian => LittleEndian
    LittleEndian => BigEndian
  }
}

///|
/// The reverse of a word order.
fn swap_word_order(order : WordOrder) -> WordOrder {
  match order {
    HighWordLowAddress => LowWordLowAddress
    LowWordLowAddress => HighWordLowAddress
  }
}

///|
/// Human keyword for a byte order, for reports and (de)serialization.
pub fn byte_order_name(order : ByteOrder) -> String {
  match order {
    BigEndian => "big-endian"
    LittleEndian => "little-endian"
  }
}

///|
/// Resolve a keyword ("big"/"little", case-insensitive) into a `ByteOrder`,
/// defaulting to big-endian for anything unrecognised.
pub fn byte_order_of(name : String) -> ByteOrder {
  let n = lowercase(name)
  if n == "little" || n == "le" || n == "littleendian" {
    LittleEndian
  } else {
    BigEndian
  }
}

///|
/// Resolve a keyword ("hi-lo"/"lo-hi", case-insensitive) into a `WordOrder`,
/// defaulting to native order (high word at the lower address).
pub fn word_order_of(name : String) -> WordOrder {
  let n = lowercase(name)
  if n == "lo-hi" || n == "lo" || n == "lowwordlowaddress" {
    LowWordLowAddress
  } else {
    HighWordLowAddress
  }
}

///|
/// Human keyword for a word order, for reports and (de)serialization.
pub fn word_order_name(order : WordOrder) -> String {
  match order {
    HighWordLowAddress => "hi-lo"
    LowWordLowAddress => "lo-hi"
  }
}

///|
/// The two bytes (ints in 0..=255) of a 16-bit word under `order`.
pub fn uint16_to_bytes(w : Int, order : ByteOrder) -> (Int, Int) {
  let hi = (w >> 8) & 0xFF
  let lo = w & 0xFF
  match order {
    BigEndian => (hi, lo)
    LittleEndian => (lo, hi)
  }
}

///|
/// Rebuild a 16-bit word from two bytes (ints in 0..=255) under `order`.
/// The pair is `(first_byte, second_byte)` in wire order.
pub fn bytes_to_uint16(b0 : Int, b1 : Int, order : ByteOrder) -> Int {
  match order {
    BigEndian => (b0 << 8) | b1
    LittleEndian => (b1 << 8) | b0
  }
}

///|
/// The unsigned 16-bit word for a coil bit (0 or 1).
pub fn encode_coil(value : Bool) -> Int {
  if value {
    1
  } else {
    0
  }
}

///|
/// Decode a coil bit from a 16-bit word; any non-zero value counts as on.
pub fn decode_coil(word : Int) -> Bool {
  (word & 0xFFFF) != 0
}

///|
/// The unsigned 16-bit word for a `uint16` value (0..=65535).
pub fn encode_uint16(v : Int) -> Int {
  v & 0xFFFF
}

///|
/// Decode a `uint16` value from a word (0..=65535).
pub fn decode_uint16(word : Int) -> Int {
  word & 0xFFFF
}

///|
/// Map a signed `int16` (-32768..=32767) onto its unsigned 16-bit word.
pub fn encode_int16(v : Int) -> Int {
  v & 0xFFFF
}

///|
/// Map an unsigned 16-bit word back to a signed `int16` (two's complement).
pub fn decode_int16(word : Int) -> Int {
  let u = word & 0xFFFF
  if u >= 0x8000 {
    u - 0x10000
  } else {
    u
  }
}

///|
/// The two bytes, in wire order for `byteOrder`, of a 16-bit value. This is the
/// minimal Modbus payload a single register occupies on the bus.
pub fn int16_bytes(v : Int, byteOrder : ByteOrder) -> Array[Int] {
  let (hi, lo) = uint16_to_bytes(v & 0xFFFF, byteOrder)
  [hi, lo]
}

///|
/// Rebuild a 16-bit word from a two-byte wire payload (ints in 0..=255).
pub fn int16_from_bytes(bytes : Array[Int], byteOrder : ByteOrder) -> Int {
  bytes_to_uint16(bytes[0] & 0xFF, bytes[1] & 0xFF, byteOrder)
}

///|
test "order keywords resolve case-insensitively" {
  assert_eq(byte_order_name(byte_order_of("LITTLE")), "little-endian")
  assert_eq(byte_order_name(byte_order_of("LittleEndian")), "little-endian")
  assert_eq(byte_order_name(byte_order_of("Big")), "big-endian")
  assert_eq(word_order_name(word_order_of("LO-HI")), "lo-hi")
  assert_eq(word_order_name(word_order_of("LoWwOrDlOwAdDrEsS")), "lo-hi")
  assert_eq(word_order_name(word_order_of("Hi-Lo")), "hi-lo")
}

///|
test "int16 byte payloads round trip and respect byte order" {
  let be = int16_bytes(0x1234, BigEndian)
  let le = int16_bytes(0x1234, LittleEndian)
  assert_eq(be, [0x12, 0x34])
  assert_eq(le, [0x34, 0x12])
  assert_eq(int16_from_bytes(be, BigEndian), 0x1234)
  assert_eq(int16_from_bytes(le, LittleEndian), 0x1234)
  // A signed view of the same payload.
  assert_eq(decode_int16(int16_from_bytes(be, BigEndian)), 4660)
}

///|
test "int16 wire round trip covers signed extremes in both byte orders" {
  let vals = [0, 1, -1, 32767, -32768, -248]
  for v in vals {
    for bo in [BigEndian, LittleEndian] {
      let bs = int16_bytes(v, bo)
      // Reassemble the unsigned word, then pull out the two's-complement sign.
      let w = int16_from_bytes(bs, bo)
      assert_eq(w & 0xFFFF, encode_int16(v))
      assert_eq(decode_int16(w & 0xFFFF), v)
    }
  }
}

///|
/// Split a 32-bit `int` into its high and low 16-bit words (unsigned).
pub fn int32_parts(v : Int) -> (Int, Int) {
  let hi = (v >> 16) & 0xFFFF
  let lo = v & 0xFFFF
  (hi, lo)
}

///|
/// Fold two unsigned 16-bit words back into a signed 32-bit `int`.
/// Because `Int` is 32-bit two's complement, `(hi << 16) | lo` already yields
/// the correct signed value for both positive and negative inputs.
pub fn int32_fold(hi : Int, lo : Int) -> Int {
  (hi << 16) | (lo & 0xFFFF)
}

///|
/// The two words of a 32-bit `int` in the register order they occupy on the
/// wire: the first element goes to the lower register address.
pub fn int32_words(v : Int, order : WordOrder) -> (Int, Int) {
  let (hi, lo) = int32_parts(v)
  match order {
    HighWordLowAddress => (hi, lo)
    LowWordLowAddress => (lo, hi)
  }
}

///|
/// Rebuild a 32-bit `int` from the two words as transmitted, given their order.
/// The pair is `(lower_address_word, higher_address_word)`.
pub fn int32_fold_words(w0 : Int, w1 : Int, order : WordOrder) -> Int {
  let atLowAddr = w0 & 0xFFFF
  let atHighAddr = w1 & 0xFFFF
  match order {
    // With native order the high word sits at the lower address, so `w0` is hi.
    HighWordLowAddress => int32_fold(atLowAddr, atHighAddr)
    // With swapped order the low word sits at the lower address, so `w1` is hi.
    LowWordLowAddress => int32_fold(atHighAddr, atLowAddr)
  }
}

///|
/// Encode a 32-bit value into a Modbus byte payload given both byte and word
/// order. Returns 4 bytes in wire order, ints in 0..=255.
pub fn encode_int32_bytes(
  v : Int,
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Array[Int] {
  let (wAtLowAddr, wAtHighAddr) = int32_words(v, wordOrder)
  let (b0, b1) = uint16_to_bytes(wAtLowAddr, byteOrder)
  let (b2, b3) = uint16_to_bytes(wAtHighAddr, byteOrder)
  [b0, b1, b2, b3]
}

///|
/// Decode a 4-byte Modbus payload back into a 32-bit `int`.
pub fn decode_int32_bytes(
  bytes : Array[Int],
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Int {
  let b0 = bytes[0] & 0xFF
  let b1 = bytes[1] & 0xFF
  let b2 = bytes[2] & 0xFF
  let b3 = bytes[3] & 0xFF
  let wAtLowAddr = bytes_to_uint16(b0, b1, byteOrder)
  let wAtHighAddr = bytes_to_uint16(b2, b3, byteOrder)
  int32_fold_words(wAtLowAddr, wAtHighAddr, wordOrder)
}

///|
test "uint16 bytes differ under big vs little endian" {
  let be = uint16_to_bytes(0x1234, BigEndian)
  let le = uint16_to_bytes(0x1234, LittleEndian)
  assert_eq(be, (0x12, 0x34))
  assert_eq(le, (0x34, 0x12))
}

///|
test "bytes and words round trip" {
  assert_eq(bytes_to_uint16(0x12, 0x34, BigEndian), 0x1234)
  assert_eq(bytes_to_uint16(0x34, 0x12, LittleEndian), 0x1234)
  // The swap of a swap is identity (compared by name, since these types have
  // no derived Eq and comparing enums directly triggers a deprecation warning).
  assert_eq(byte_order_name(swap_byte_order(BigEndian)), "little-endian")
  assert_eq(word_order_name(swap_word_order(LowWordLowAddress)), "hi-lo")
}

///|
test "int16 two's complement maps negative values" {
  assert_eq(encode_int16(0), 0)
  assert_eq(encode_int16(-1), 0xFFFF)
  assert_eq(encode_int16(32767), 0x7FFF)
  assert_eq(decode_int16(0xFFFF), -1)
  assert_eq(decode_int16(0x8000), -32768)
  assert_eq(decode_int16(0x7FFF), 32767)
  assert_eq(decode_int16(encode_int16(-249)), -249)
}

///|
test "coil maps to 0 or 1 and decodes any non-zero as on" {
  assert_eq(encode_coil(true), 1)
  assert_eq(encode_coil(false), 0)
  assert_eq(decode_coil(1), true)
  assert_eq(decode_coil(0), false)
  assert_eq(decode_coil(0x8000), true)
}

///|
test "int32_parts separates high and low words" {
  assert_eq(int32_parts(0x12345678), (0x1234, 0x5678))
  assert_eq(int32_fold(0x1234, 0x5678), 0x12345678)
  // Negative values keep the two's-complement high word.
  let (hi, lo) = int32_parts(-1)
  assert_eq(hi, 0xFFFF)
  assert_eq(lo, 0xFFFF)
  assert_eq(int32_fold(0xFFFF, 0xFFFF), -1)
}

///|
test "int32_words respects word order" {
  assert_eq(int32_words(0x12345678, HighWordLowAddress), (0x1234, 0x5678))
  assert_eq(int32_words(0x12345678, LowWordLowAddress), (0x5678, 0x1234))
  assert_eq(int32_fold_words(0x1234, 0x5678, HighWordLowAddress), 0x12345678)
  assert_eq(int32_fold_words(0x5678, 0x1234, LowWordLowAddress), 0x12345678)
}

///|
test "int32 byte encoding round trips under every endianness" {
  let values = [0, 1, -1, 32767, -32768, 0x12345678]
  for v in values {
    let be = encode_int32_bytes(v, BigEndian, HighWordLowAddress)
    assert_eq(decode_int32_bytes(be, BigEndian, HighWordLowAddress), v)
    let le = encode_int32_bytes(v, LittleEndian, LowWordLowAddress)
    assert_eq(decode_int32_bytes(le, LittleEndian, LowWordLowAddress), v)
  }
}

///|
test "swapping byte or word order changes the encoded payload" {
  let a = encode_int32_bytes(0x12345678, BigEndian, HighWordLowAddress)
  let b = encode_int32_bytes(0x12345678, LittleEndian, HighWordLowAddress)
  let c = encode_int32_bytes(0x12345678, BigEndian, LowWordLowAddress)
  assert_eq(a[0], 0x12)
  assert_eq(c[0], 0x56) // low word moved to the lower address
  let mut differ = 0
  for k = 0; k < a.length(); k = k + 1 {
    if a[k] != b[k] {
      differ = differ + 1
    }
  }
  assert_eq(differ > 0, true)
}

// ---------------------------------------------------------------------------
// Floating point.
//
// float64 is exact: the value's IEEE-754 bit pattern is reinterpreted directly
// (double_bits / double_from_bits), so normal, subnormal, ±0, ±Inf and NaN all
// round-trip bit-for-bit.
//
// float32 turns a double into single precision with the backend's IEEE-754
// round-to-nearest, ties-to-even demotion, then reads the resulting 32-bit
// pattern. That reproduces correct rounding at midpoints and at the
// normal/subnormal and overflow boundaries.
//
// The APIs use a fixed big-endian word order (most-significant word first).
// Per-word byte order is supported through the *bytes helpers, reusing
// `uint16_to_bytes` / `bytes_to_uint16`.
// ---------------------------------------------------------------------------

///|
/// The 16-bit WORD PACK that a double occupies, high word first.
fn split_words64(b : Int64) -> (Int, Int, Int, Int) {
  let w0 = ((b >> 48) & 0xFFFFL).to_int()
  let w1 = ((b >> 32) & 0xFFFFL).to_int()
  let w2 = ((b >> 16) & 0xFFFFL).to_int()
  let w3 = (b & 0xFFFFL).to_int()
  (w0, w1, w2, w3)
}

///|
/// The reverse of `split_words64`.
fn compose_words64(w0 : Int, w1 : Int, w2 : Int, w3 : Int) -> Int64 {
  (w0.to_int64() << 48) |
  (w1.to_int64() << 32) |
  (w2.to_int64() << 16) |
  w3.to_int64()
}

///|
/// IEEE-754 double-precision bits for `v`. This is an exact bit-preserving
/// reinterpretation, so every pattern (normal, subnormal, ±0, ±Inf, NaN) is
/// represented faithfully.
fn double_bits(v : Double) -> Int64 {
  v.reinterpret_as_int64()
}

///|
/// Rebuild the exact `Double` from its IEEE-754 bit pattern.
fn double_from_bits(b : Int64) -> Double {
  b.reinterpret_as_double()
}

///|
/// The four 16-bit words (high word first) of a double-precision value.
pub fn float64_words(v : Double) -> (Int, Int, Int, Int) {
  split_words64(double_bits(v))
}

///|
/// Rebuild a double-precision value from its four words (high word first).
pub fn float64_from_words(w0 : Int, w1 : Int, w2 : Int, w3 : Int) -> Double {
  double_from_bits(compose_words64(w0, w1, w2, w3))
}

///|
/// The eight bytes (in wire order) of a double value, honouring both byte order
/// and word order. The four words are laid out most-significant word first under
/// `HighWordLowAddress`, and the whole sequence is reversed under
/// `LowWordLowAddress`.
pub fn float64_bytes(
  v : Double,
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Array[Int] {
  let (w0, w1, w2, w3) = float64_words(v)
  let (a, b, c, d) = match wordOrder {
    HighWordLowAddress => (w0, w1, w2, w3)
    LowWordLowAddress => (w3, w2, w1, w0)
  }
  let (h0, l0) = uint16_to_bytes(a, byteOrder)
  let (h1, l1) = uint16_to_bytes(b, byteOrder)
  let (h2, l2) = uint16_to_bytes(c, byteOrder)
  let (h3, l3) = uint16_to_bytes(d, byteOrder)
  [h0, l0, h1, l1, h2, l2, h3, l3]
}

///|
/// Rebuild a double value from eight bytes produced by `float64_bytes`, given
/// their byte order and word order.
pub fn float64_from_bytes(
  bytes : Array[Int],
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Double {
  let a = bytes_to_uint16(bytes[0] & 0xFF, bytes[1] & 0xFF, byteOrder)
  let b = bytes_to_uint16(bytes[2] & 0xFF, bytes[3] & 0xFF, byteOrder)
  let c = bytes_to_uint16(bytes[4] & 0xFF, bytes[5] & 0xFF, byteOrder)
  let d = bytes_to_uint16(bytes[6] & 0xFF, bytes[7] & 0xFF, byteOrder)
  let (w0, w1, w2, w3) = match wordOrder {
    HighWordLowAddress => (a, b, c, d)
    LowWordLowAddress => (d, c, b, a)
  }
  float64_from_words(w0, w1, w2, w3)
}

///|
/// IEEE-754 single-precision bits for `v` (a `Double`, rounded to float32).
fn float32_bits(v : Double) -> Int {
  if v != v {
    // Canonical quiet NaN, kept deterministic across backends.
    return 0x7FC00000
  }
  // Demote to single precision with IEEE-754 round-to-nearest, ties-to-even,
  // then read the exact 32-bit pattern. This gets midpoints, overflow to
  // infinity and the normal/subnormal boundary right by construction.
  Float::from_double(v).reinterpret_as_int()
}

///|
/// Rebuild a single-precision value (as a `Double`) from its 32-bit bits.
fn float32_from_bits(bits : Int) -> Double {
  let sign = (bits >> 31) & 1
  let exp = (bits >> 23) & 0xFF
  let mant = bits & 0x7FFFFF
  if exp == 0 {
    // Subnormal (or zero): value = mant * 2^-149, no implicit leading one.
    let mag = if mant == 0 {
      0.0
    } else {
      mant.to_double() * @math.pow(2.0, -149.0)
    }
    if sign == 1 {
      -mag
    } else {
      mag
    }
  } else if exp == 0xFF {
    // Special sentinel: all-ones exponent. Mantissa zero is ±Infinity, a
    // non-zero mantissa is a NaN (any quiet NaN widens to a double NaN).
    if mant == 0 {
      if sign == 1 {
        0xFFF0000000000000L.reinterpret_as_double()
      } else {
        0x7FF0000000000000L.reinterpret_as_double()
      }
    } else {
      0x7FF8000000000001L.reinterpret_as_double()
    }
  } else {
    let mag = (8388608 | mant).to_double() *
      @math.pow(2.0, (exp - 150).to_double())
    if sign == 1 {
      -mag
    } else {
      mag
    }
  }
}

///|
/// The two 16-bit words (high word first) of a single-precision value.
pub fn float32_words(v : Double) -> (Int, Int) {
  let b = float32_bits(v)
  let hi = (b >> 16) & 0xFFFF
  let lo = b & 0xFFFF
  (hi, lo)
}

///|
/// Rebuild a single-precision value from its two words (high word first).
pub fn float32_from_words(hi : Int, lo : Int) -> Double {
  float32_from_bits(((hi & 0xFFFF) << 16) | (lo & 0xFFFF))
}

///|
/// The four bytes (in wire order) of a single-precision value, honouring both
/// byte order and word order. The high word sits at the lower address under
/// `HighWordLowAddress`, and the low word does under `LowWordLowAddress`.
pub fn float32_bytes(
  v : Double,
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Array[Int] {
  let (hi, lo) = float32_words(v)
  let (a, b) = match wordOrder {
    HighWordLowAddress => (hi, lo)
    LowWordLowAddress => (lo, hi)
  }
  let (b0, b1) = uint16_to_bytes(a, byteOrder)
  let (b2, b3) = uint16_to_bytes(b, byteOrder)
  [b0, b1, b2, b3]
}

///|
/// Rebuild a single-precision value from four bytes, given their byte order and
/// word order.
pub fn float32_from_bytes(
  bytes : Array[Int],
  byteOrder : ByteOrder,
  wordOrder : WordOrder,
) -> Double {
  let a = bytes_to_uint16(bytes[0] & 0xFF, bytes[1] & 0xFF, byteOrder)
  let b = bytes_to_uint16(bytes[2] & 0xFF, bytes[3] & 0xFF, byteOrder)
  let (hi, lo) = match wordOrder {
    HighWordLowAddress => (a, b)
    LowWordLowAddress => (b, a)
  }
  float32_from_words(hi, lo)
}

///|
test "float64 round trips 1.0 through words" {
  let (w0, w1, w2, w3) = float64_words(1.0)
  // 1.0 = 0x3FF00000_00000000, so w0=0x3FF0, w1=0, then zeros.
  assert_eq(w0, 0x3FF0)
  assert_eq(w1, 0)
  assert_eq(float64_from_words(w0, w1, w2, w3), 1.0)
}

///|
test "float64 round trips a spread of values" {
  let values = [1.0, -1.0, 0.0, -0.0, 3.0, 1234.5, 0.5, -2.25]
  for v in values {
    let (w0, w1, w2, w3) = float64_words(v)
    assert_eq(float64_from_words(w0, w1, w2, w3), v)
    for bo in [BigEndian, LittleEndian] {
      for wo in [HighWordLowAddress, LowWordLowAddress] {
        let bs = float64_bytes(v, bo, wo)
        assert_eq(float64_from_bytes(bs, bo, wo), v)
      }
    }
  }
}

///|
test "float64 bytes differ under big and little endian" {
  let be = float64_bytes(1.0, BigEndian, HighWordLowAddress)
  let le = float64_bytes(1.0, LittleEndian, HighWordLowAddress)
  assert_eq(be[0], 0x3F)
  assert_eq(be[1], 0xF0)
  // Little endian flips each word's byte order (low byte first).
  assert_eq(le[0], 0xF0)
  assert_eq(le[1], 0x3F)
}

///|
test "float32/float64 byte order and word order round trip" {
  // 1.5f = 0x3FC00000; with the low word at the lower address the words swap.
  assert_eq(float32_bytes(1.5, BigEndian, HighWordLowAddress), [
    0x3F, 0xC0, 0x00, 0x00,
  ])
  assert_eq(float32_bytes(1.5, BigEndian, LowWordLowAddress), [
    0x00, 0x00, 0x3F, 0xC0,
  ])
  for bo in [BigEndian, LittleEndian] {
    for wo in [HighWordLowAddress, LowWordLowAddress] {
      assert_eq(float32_from_bytes(float32_bytes(1.5, bo, wo), bo, wo), 1.5)
      assert_eq(
        float64_from_bytes(float64_bytes(1234.5, bo, wo), bo, wo),
        1234.5,
      )
    }
  }
}

///|
test "float32 round trips values that are exactly representable" {
  let values = [1.5, -0.25, 3.0, 0.0, -0.0, 1234.0, 0.5]
  for v in values {
    let (hi, lo) = float32_words(v)
    assert_eq(float32_from_words(hi, lo), v)
    let be = float32_bytes(v, BigEndian, HighWordLowAddress)
    assert_eq(float32_from_bytes(be, BigEndian, HighWordLowAddress), v)
  }
}

///|
test "float32 encodes 1.5 to the expected words" {
  // 1.5 = sign 0, exp 127, mantissa 0x400000 => words hi=0x3FC0, lo=0x0000.
  let (hi, lo) = float32_words(1.5)
  assert_eq(hi, 0x3FC0)
  assert_eq(lo, 0)
}

///|
test "float32 rounds midpoints to even (IEEE-754)" {
  // 1 + 2^-24 is exactly halfway between 1.0 and 1.0 + 2^-23; ties-to-even
  // selects the lower (even-mantissa) neighbour.
  let mid = 1.0 + 1.0 / 16777216.0
  let (hi, lo) = float32_words(mid)
  assert_eq(hi, 0x3F80)
  assert_eq(lo, 0x0000)
}

///|
test "float32 overflows to infinity instead of wrapping" {
  let (hi1, lo1) = float32_words(1.0e39)
  assert_eq(hi1, 0x7F80)
  assert_eq(lo1, 0x0000)
  let (hi2, lo2) = float32_words(@math.pow(2.0, 129.0))
  assert_eq(hi2, 0x7F80)
  assert_eq(lo2, 0x0000)
}

///|
test "float32 promotes the subnormal/normal midpoint to the smallest normal" {
  let smallest_normal = @math.pow(2.0, -126.0)
  // Exactly halfway between the largest subnormal and the smallest normal;
  // ties-to-even selects the smallest normal (even mantissa).
  let mid = smallest_normal - smallest_normal / 16777216.0
  let (hi, lo) = float32_words(mid)
  assert_eq(hi, 0x0080)
  assert_eq(lo, 0x0000)
  assert_eq(float32_from_words(hi, lo), smallest_normal)
}

// ---------------------------------------------------------------------------
// Integer boundary tests.
// ---------------------------------------------------------------------------

///|
test "int32 extremes round trip through words and bytes" {
  let vals = [2147483647, -2147483648, 0, -1, 1]
  for v in vals {
    let (hi, lo) = int32_words(v, HighWordLowAddress)
    assert_eq(int32_fold_words(hi, lo, HighWordLowAddress), v)
    for bo in [BigEndian, LittleEndian] {
      for wo in [HighWordLowAddress, LowWordLowAddress] {
        let bs = encode_int32_bytes(v, bo, wo)
        assert_eq(decode_int32_bytes(bs, bo, wo), v)
      }
    }
  }
}

///|
test "uint32 bit patterns at the upper half of the range" {
  // 0x7FFFFFFF.
  let (hi1, lo1) = int32_parts(2147483647)
  assert_eq(hi1, 0x7FFF)
  assert_eq(lo1, 0xFFFF)
  // 0x80000000 (bit 31 set) is -2147483648 in two's complement.
  let (hi2, lo2) = int32_parts(-2147483648)
  assert_eq(hi2, 0x8000)
  assert_eq(lo2, 0x0000)
  // 0xFFFFFFFF (all bits set) is -1 in two's complement.
  let (hi3, lo3) = int32_parts(-1)
  assert_eq(hi3, 0xFFFF)
  assert_eq(lo3, 0xFFFF)
  assert_eq(int32_fold(hi2, lo2), -2147483648)
  assert_eq(int32_fold(hi3, lo3), -1)
}

// ---------------------------------------------------------------------------
// Floating-point boundary tests.
// ---------------------------------------------------------------------------

///|
test "float64 edge cases: largest, smallest normal, subnormal, inf, nan" {
  let largest = 1.7976931348623157e308
  let (l0, l1, l2, l3) = float64_words(largest)
  assert_eq(float64_from_words(l0, l1, l2, l3), largest)
  let smallest = 4.9406564584124654e-324
  let (s0, s1, s2, s3) = float64_words(smallest)
  assert_eq(float64_from_words(s0, s1, s2, s3), smallest)
  // +Infinity produces the double pattern 0x7FF0 0000 0000 0000.
  let (i0, i1, i2, i3) = float64_words(
    0x7FF0000000000000L.reinterpret_as_double(),
  )
  assert_eq(i0, 0x7FF0)
  assert_eq(i1, 0)
  assert_eq(i2, 0)
  assert_eq(i3, 0)
  // NaN round trips: re-encoding its words still yields a NaN (self-unequal).
  let nan = 0x7FF8000000000001L.reinterpret_as_double()
  let (a, b, c, d) = float64_words(nan)
  let nan2 = float64_from_words(a, b, c, d)
  assert_eq(nan2 != nan2, true)
}

///|
test "float32 edge cases: normal bounds, subnormal, inf, and NaN pattern" {
  let hi_normal = 3.4028234663852886e38
  let (h0, h1) = float32_words(hi_normal)
  assert_eq(float32_from_words(h0, h1), hi_normal)
  let lo_normal = 1.1754943508222875e-38
  let (n0, n1) = float32_words(lo_normal)
  assert_eq(float32_from_words(n0, n1), lo_normal)
  // Smallest positive subnormal float32 is 2^-149.
  let sub = 1.401298464324817e-45
  let (u0, u1) = float32_words(sub)
  assert_eq(float32_from_words(u0, u1), sub)
  // Decoding the float32 +inf pattern gives a positive infinity.
  assert_eq(float32_from_words(0x7F80, 0x0000) > 3.402823466e38, true)
  // Decoding a quiet-NaN pattern yields a NaN (self-unequal).
  let nan = float32_from_words(0x7FC0, 0x0000)
  assert_eq(nan != nan, true)
}