Ship

Old Docs · String And Bytes
On this page · 9 sections

String And Bytes

A Ship String is a valid sequence of Unicode scalar values. It is not List(Char), not a random-access collection, and not JavaScript's UTF-16 string. Wire data is Bytes. UI graphemes and display width are library layers, not the core type.

Decisions

  • Byte is one UTF-8 code unit (0–255). 'a' / '\n' / '\xNN' literals and patterns; inclusive ranges '0'..'9'. No arithmetic. Explicit Byte.to_int / Byte.of_int.

  • Bytes is the immutable wire buffer. O(1) length / get. Parsers scan Bytes / Bytes.Slice with while and byte patterns.

  • Char is one Unicode scalar value (U+0000–U+10FFFF except surrogates). c'é' is exactly one scalar; grapheme clusters with multiple scalars are rejected as Char literals.

  • String is immutable, valid Unicode scalars. Equality, hashing, ordering, and literal patterns use the exact scalar sequence. Storage is not normalized.

  • Owning String / Bytes stay compact. String.Slice / Bytes.Slice are explicit views that may retain backing storage. Converting a slice to an owning value is the copy/re-home boundary. Sharing is an optimization, not a cross-target allocation promise.

  • No integer String.length / peek / slice. Named units: byte_length, char_count (explicitly O(n)), scalar iteration, opaque cursors.

  • Prefix matching is the inverse of concat: "GET " ++ rest / b"GET " ++ rest. Rest is a Slice. Expression ++ concatenates String only — there is no Bytes ++ Bytes expression; wire concat is Bytes.Builder. + stays numeric. :: is not list cons here (and is unused). Dynamic needles use strip_prefix.

  • Native storage is UTF-8. JavaScript may store a well-formed UTF-16 string privately. Source semantics do not expose host .length, indexing, or .slice.

  • Elixir/Erlang <<>> bit-syntax is deferred: ../questions/239-bytes-bit-syntax.md.

  • Graphemes, NFC, and terminal width: ../questions/182-unicode-segmentation-and-normalization.md.

Layers

Byte / Bytes          machine and wire
Char / String         Unicode scalars and valid `String`
Grapheme view         versioned UI segmentation (library)
Display width         renderer/terminal-specific (library)

String may contain NUL, private-use, unassigned, and noncharacter scalars. Malformed UTF-8 belongs in Bytes. Decode at the String/Bytes edge:

let string = String.of_bytes(bytes); // exn :utf8(Int)

Literals

SpellingTypeNotes
'a', '\n', '\x1f'ByteASCII / byte escapes
c'é', c'\u{1F980}'CharExactly one scalar
"GET "StringValid scalars; interpolation uses Display
#"{"id":1}"#, ##"a "# b"##StringVerbatim: no escapes, newlines kept; holes are $#{…} with the literal's fence
b"GET ", b"\x89PNG"BytesUTF-8 for ordinary chars; \xNN for arbitrary bytes; no interpolation

Byte escapes: \n, \r, \t, \\, \', \", \0, \xNN. Char literals also accept \u{…} for a scalar. c'e\u{0301}' is two scalars and is a compile error.

Matching

Exact literals match the whole value. Byte and Char support inclusive ranges. String and Bytes are open domains and need _.

let classify_byte = function {
  | '0'..'9' -> :ascii_digit
  | '\r' | '\n' -> :line_break_byte
  | _ -> :other_byte
};

let classify_spelling = function {
  | "é" -> :precomposed
  | "e\u{0301}" -> :decomposed
  | _ -> :other
};

let classify_packet = function {
  | b"\x89PNG\r\n\x1a\n" -> :png_signature
  | b"PING" -> :ping
  | _ -> :other
};

Prefix patterns invert concat. The left side is a constant sequence or a singleton Char/Byte (including ranges). The right side is a binder, _, or another ++ pattern. No suffix patterns. No runtime-variable prefix.

match input {
  | b"GET " ++ rest -> Get(rest)
  | b"POST " ++ rest -> Post(rest)
  | b"GET " ++ "/" ++ path -> GetPath(path)
  | _ -> Unknown(input)
}

match string {
  | "Hello, " ++ name -> String.of_slice(name)
  | c'a'..c'z' ++ _ | c'A'..c'Z' ++ _ -> :letter
  | "" -> :empty
  | _ -> :other
}

First match wins. "GET" ++ rest before "GETTING" ++ rest makes the longer arm dead — warn; do not longest-match; do not reorder arms. Prefix arms never exhaust String/Bytes.

String prefixes consume whole scalars. Bytes prefixes consume bytes and may split a scalar; decoding that rest as String still requires a UTF-8 boundary.

Slices And Cursors

let rest: Bytes.Slice = match input {
  | b"GET " ++ rest -> rest
  | _ -> Bytes.as_slice(input)
};
let owned = Bytes.of_slice(rest); // may copy; drops the parent reference

Views store owner + range, never interior pointers. Nested slices canonicalize to the original backing. A tiny slice of a huge buffer can retain that buffer until of_slice / String.of_slice copies.

Opaque String.Cursor values come from iteration and search. They are not integer scalar offsets.

Builders

String.Builder / Bytes.Builder are mutable scratch buffers. push / push_char / push_byte are amortized O(1) and return Unit. finish takes the accumulated buffer: native hands it off; JavaScript joins parts into a new host string / Uint8Array and clears the builder so a later push starts empty, matching native take. Uniqueness (Builder @ unique, finish consuming the value) is the modes story in 16-modes.md, not an enforced property of today's heap objects — do not let a builder escape the function that created it.

Do not accumulate with acc ++ ch in a loop.

Targets

FactNativeJavaScript
String contentsUTF-8 byteswell-formed JS string (UTF-16)
Bytesno-scan byte payloadUint8Array
String ↔ Bytesshare backing when UTF-8 is knowncopy (TextEncoder / fatal UTF-8 decode)
Bytes.Sliceowner + range over the same ByteBacking{ $base, $start, $end }
String.Slice / Cursorowner + range (UTF-8 offsets)same wrapper; indices are UTF-16 units internally
Observable semanticsexact scalars, UTF-8 decode, ++ prefixessame

Zero-copy is not portable source semantics. Values, validity, and match behavior are. The interpreter stores owning String/Bytes as views over a shared no-scan ByteBacking (Vec<u8> plus a “known UTF-8” flag).

Rejected

  • Grapheme-first String / Char (Swift)

  • Codepoint-indexed String.length / peek / slice (Python)

  • Byte-oriented String that may hold invalid UTF-8 (OCaml/Go)

  • String ≡ Bytes (Elixir)

  • Copy-on-write strings as the user model

  • + as string concat or prefix pattern

  • :: as string/bytes prefix (list cons of an element)

  • Host JavaScript .length / indexing / .slice as Ship units

  • Normalizing storage or making == canonical equivalence

See Also