How do I work with raw bytes in EK9?

← Advanced Type System · Ref: Q1382

Byte is an unsigned 8-bit value - 0 to 255 - and it is the raw-data scalar, not a small number. It is tri-state like every EK9 type.

CREATING

There is no Byte literal. Construct from an Integer, from hex text, or by copy:

  fromInteger <- Byte(255)
  fromHex <- Byte("0xFF")     // 0x prefix optional
  bare <- Byte("FF")
  copied <- Byte(fromInteger)

Out of range gives UNSET rather than a wrapped value, so a bad octet is visibly absent instead of quietly wrong:

  Byte(256)?    // false
  Byte(-1)?     // false

NO ARITHMETIC - THIS IS DELIBERATE

Byte has NO +, -, *, / and no ++ or --. The question 'what is 0xFF + 1?' cannot be asked of a Byte at all, so the wraparound bug class does not exist. Java answers it by making byte SIGNED (0xFF reads back as -1, hence the b & 0xFF idiom everywhere), C leaves it to integer promotion, Go wraps silently to 0. EK9 removes the question.

To do arithmetic, convert deliberately:

  total <- aByte.toInteger() + 1

Or let #^ promote inside an Integer expression. Note the asymmetry - promotion applies to the ARGUMENT:
- `1 + #^aByte` resolves, and widens: 1 + 0xFF is 256, never 0
- `aByte + 1` does NOT resolve - Byte has no + of its own

HEX IN, HEX OUT

$ emits exactly two upper-case hex characters and the String constructor parses hex, so the round trip always holds:
- `$Byte(15)` gives "0F"
- `Byte($b) == b` is true for all 256 values
Decimal text is deliberately rejected - Byte("255") is UNSET - because "10" cannot mean sixteen to $ and ten to the constructor. Decimal arrives through Byte(Integer).

BITWISE AND SHIFTS

and, or, xor and ~ work as they do on Bits and Boolean. Shifts stay inside eight bits, so bits shifted out are lost and the value never widens:
- `Byte("0F") << 4` gives F0
- `Byte("0F") << 8` gives 00
>> is always a logical shift because a Byte is unsigned - there is no sign bit to propagate.

CONVERSIONS

- `aByte.toInteger()` gives 0-255
- `aByte.toBits()` gives the eight-bit sequence
- `$$aByte` gives a JSON number 0-255, not the hex text

See Q40 for Bits (a variable-length bit sequence, a different job). Use 'ek9 -h Byte' for the full API.

Example

defines module qa.byte.usage

  defines program

    ByteUsage()
      stdout <- Stdout()

      //Construct from hex text or from an Integer - there is no Byte literal.
      fromHex <- Byte("0xFF")
      stdout.println(`fromHex: ${fromHex}`)

      //$ always emits exactly two upper-case hex characters.
      fifteen <- Byte(15)
      stdout.println(`fifteen: ${fifteen}`)

      //Out of range is UNSET, never a wrapped value.
      outOfRange <- Byte(256)
      stdout.println(`outOfRange set: ${outOfRange?}`)

      //Decimal text is not a Byte - the String form is hex in both directions.
      decimalText <- Byte("255")
      stdout.println(`decimalText set: ${decimalText?}`)

      //Bitwise operators, as on Bits and Boolean.
      masked <- Byte("AA") and Byte("0F")
      stdout.println(`masked: ${masked}`)

      //Shifts stay inside eight bits - what leaves is lost, the value never widens.
      shifted <- Byte("0F") << 4
      stdout.println(`shifted: ${shifted}`)

      shiftedOut <- Byte("0F") << 8
      stdout.println(`shiftedOut: ${shiftedOut}`)

      //No arithmetic on Byte - promote instead, and it widens rather than wrapping.
      widened <- 1 + #^fromHex
      stdout.println(`widened: ${widened}`)

      //Conversions.
      asBits <- Byte("AA").toBits()
      stdout.println(`asBits: ${asBits}`)

      //The contract that justifies hex-only parsing.
      roundTripped <- Byte($fifteen)
      stdout.println(`roundTrip holds: ${roundTripped == fifteen}`)
Other ways to ask this
  • Is there a byte type in EK9?
  • How do I represent a single octet of binary data?
  • Why can I not add two Bytes together?
  • How do I convert a Byte to hex text and back?

Coming from another language?

Java: byte is SIGNED -128..127, so reading 0xFF from a stream gives -1 and nearly every use is written 'b & 0xFF'; forgetting the mask is a classic binary-parsing bug the compiler cannot see. C: unsigned char is 0-255 but participates in silent integer promotion. Python: no byte scalar at all - indexing a bytes object yields a full-range int. Go: byte is uint8 and arithmetic wraps silently (0xFF + 1 == 0). Rust: u8 arithmetic panics on overflow in debug and wraps in release. EK9: Byte is unsigned 0-255 with NO arithmetic operators, so overflow cannot be expressed; out-of-range construction is UNSET, not wrapped; the String form is hex in BOTH directions so $ round-trips.

Keywords: raw, data, shift, bitwise, overflow, hexadecimal, unsigned, tristate, octet, byte, binary, hex