05 — Types and strings

Previous: Control flow

日本語版: 05_types_strings.vibe.md

You have been using Int, Double, Bool and String (and Char, which is another name for Int) without being told much about them, and mostly that has been fine. This chapter covers the two places where it stops being fine: text is bytes, and printing a value needs a way to print it.

The exact numeric ranges are at the end of the chapter, where you can find them when a number surprises you.

String is a string of bytes

This is the one that surprises people. s[i] is an Int — the byte at that offset — not a one-character String. Indices and lengths are byte counts.

fn main allows Console {
  let s = "hello"
  println("s[0] = \{s[0]}")
  println("from_byte = \{String::from_byte(s[0])}")
  println("s[:] = \{s[:]}")
  println("s[:2] = \{s[:2]}")
  println("s[2:] = \{s[2:]}")
  println("s[1:4] = \{s[1:4]}")
  println("length = \{String::length(s)}")
}
s[0] = 104
from_byte = h
s[:] = hello
s[:2] = he
s[2:] = llo
s[1:4] = ell
length = 5

String::from_byte writes a byte back out as a one-byte String — String::from_char_code is its older name. It does not encode a code point: hand it 233 (the code point of é) and you get a lone 0xE9 byte, which is not valid UTF-8 (#2203). The four slice forms — s[:], s[:n], s[n:], s[a:b] — work the same on Bytes and on Array[T].

Because indices are bytes, slicing does not respect Unicode code point boundaries. Slicing ASCII is safe; slicing arbitrary text at an arbitrary index is not, and that is a deliberate choice — the memory is bytes, so the type says bytes rather than pretending otherwise.

Nothing checks that a String holds valid UTF-8, and invalid bytes survive inside the program until something outside it has to read them as text:

is three bytes long, and byte 1 is still 233.

and vibe test decodes its output as UTF-8, so each invalid sequence appears as � (U+FFFD). The replacement happens on the way out; the String itself is not changed.

valid UTF-8, so returning such a value from a component export fails when the runtime lifts it (wasmtime reports incomplete utf-8 byte sequence) instead of delivering altered text.

Interpolation

"hello \{x}" is the only interpolation syntax, and any expression fits in the braces. A value can be interpolated when the compiler can find a to_string for its type: scalars, Option, tuples and arrays already have one, and your own types get one from derive (Show).

struct Point {
  x: Int; y: Int
} derive (Show)

fn main allows Console {
  let p = Point::{
    x: 3, y: 4
  }
  println("p = \{p}")
  println("opt = \{Some(1)}")
  println("pair = \{(2, 3)}")
}
p = Point { x: 3, y: 4 }
opt = Some(1)
pair = (2, 3)

Leave derive (Show) off and interpolating a Point is a compile error, which is the intended answer — printing an address instead would be a wrong one.

Decimals

A decimal literal is a Double — 64-bit. A 32-bit Float takes an f suffix: 1.5f. Both interpolate as the decimal you wrote.

How far Int goes

Reach for this section when a number does something you did not expect; until then the short version is that Int holds whole numbers and overflow does not trap.

Int is 63 bits wide, not 64. The range is -2^62 .. 2^62-1, and a literal above 4611686018427387903 is rejected rather than truncated. Arithmetic that goes out of range wraps, as 63-bit two's complement, identically on every backend — so max + 1 is min:

fn main allows Console {
  let max = 4611686018427387903
  println("max = \{max}")
  println("max + 1 = \{max + 1}")
  println("hex 0xFF = \{0xFF}")
  println("1 << 4 = \{1 << 4}")
  let neg = -8
  println("(-8) >> 1 = \{neg >> 1}")
  println("~5 = \{~5}")
  println("~(-8) = \{~neg}")
}
max = 4611686018427387903
max + 1 = -4611686018427387904
hex 0xFF = 255
1 << 4 = 16
(-8) >> 1 = -4
~5 = -6
~(-8) = 7

Three things to know coming from a C-family language:

shift, mask afterwards.

its own inverse. It binds like the other prefix operators, tighter than the infix ones.

for BigInt in @vibe/core deliberately.

Next: Mutation, regions, and escape.