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:
- Interpolation copies the bytes unchanged.
"<\{String::from_byte(233)}>"
is three bytes long, and byte 1 is still 233.
- Printing hands the bytes to the host, and the runner behind
vibe run
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.
- A WIT boundary does not accept them at all. A WIT
stringmust be
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:
>>is arithmetic — it sign-extends. There is no>>>; for a logical
shift, mask afterwards.
~is the complement over the 63-bit value, so~x == -x - 1and it is
its own inverse. It binds like the other prefix operators, tighter than the infix ones.
- Nothing is promoted silently. When you need more than 63 bits, reach
for BigInt in @vibe/core deliberately.