2
This commit is contained in:
585
2_types_variables.md
Normal file
585
2_types_variables.md
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
# Chapter 02 — Types & Variables
|
||||||
|
|
||||||
|
> **Prerequisites:** Chapter 01 complete. You can build and run a Zig project.
|
||||||
|
> **Goal:** Master Zig's type system — integers, floats, booleans, optionals, type casting, comptime types, and the strict no-implicit-coercion rules that will bite you coming from Rust.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.1 No Implicit Coercion — The Golden Rule
|
||||||
|
|
||||||
|
This is the single most important thing to internalize before anything else.
|
||||||
|
|
||||||
|
In Rust, some coercions are implicit:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let x: u32 = 5;
|
||||||
|
let y: u64 = x; // OK in Rust — implicit coercion from u32 to u64
|
||||||
|
```
|
||||||
|
|
||||||
|
**In Zig, this is a compile error.** Every type conversion must be explicit.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x: u32 = 5;
|
||||||
|
const y: u64 = x; // ERROR: expected type 'u64', found 'u32'
|
||||||
|
```
|
||||||
|
|
||||||
|
There are exactly two exceptions:
|
||||||
|
1. **`comptime_int` and `comptime_float`** — untyped compile-time constants coerce freely to any compatible concrete type.
|
||||||
|
2. **Peer type resolution** — in specific places (like `if`/`else` arms), Zig picks a common type if one side can coerce to the other.
|
||||||
|
|
||||||
|
Everything else: you cast explicitly. This is a feature. It means you always know exactly what's happening to your bits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 Integer Types
|
||||||
|
|
||||||
|
### Fixed-width integers
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const a: u8 = 255;
|
||||||
|
const b: u16 = 65535;
|
||||||
|
const c: u32 = 4_294_967_295; // underscore separators are fine
|
||||||
|
const d: u64 = 18_446_744_073_709_551_615;
|
||||||
|
const e: u128 = 0;
|
||||||
|
|
||||||
|
const f: i8 = -128;
|
||||||
|
const g: i16 = -32768;
|
||||||
|
const h: i32 = -2_147_483_648;
|
||||||
|
const i: i64 = -9_223_372_036_854_775_808;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arbitrary-width integers — embedded gold
|
||||||
|
|
||||||
|
This is something Rust doesn't have natively. Zig lets you declare integers of any bit width from `u0` to `u65535`:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const nibble: u4 = 0b1010; // 4-bit unsigned — perfect for BCD
|
||||||
|
const flags: u3 = 0b101; // 3-bit field
|
||||||
|
const reg: u12 = 0xABC; // 12-bit ADC value
|
||||||
|
const addr: u20 = 0xFFFFF; // 20-bit address bus
|
||||||
|
|
||||||
|
// Signed arbitrary-width
|
||||||
|
const offset: i4 = -8; // 4-bit signed two's complement
|
||||||
|
```
|
||||||
|
|
||||||
|
In embedded, this is huge. A 12-bit ADC reading is actually `u12`, not `u16` with wasted bits. Packed structs (Chapter 06) use these to map hardware registers exactly.
|
||||||
|
|
||||||
|
### `usize` and `isize`
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const ptr_size: usize = @sizeOf(*u8); // pointer-sized unsigned
|
||||||
|
const offset: isize = -1; // pointer-sized signed
|
||||||
|
```
|
||||||
|
|
||||||
|
Same semantics as Rust's `usize`/`isize`. Use `usize` for lengths, indices, and memory offsets.
|
||||||
|
|
||||||
|
### Integer literals
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const dec = 1_000_000; // decimal
|
||||||
|
const hex = 0xFF_AB_12; // hexadecimal
|
||||||
|
const oct = 0o777; // octal
|
||||||
|
const bin = 0b1101_0010; // binary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 `comptime_int` and `comptime_float`
|
||||||
|
|
||||||
|
When you write a literal without a type annotation, Zig gives it a special **comptime-only type**:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = 42; // type is comptime_int — has arbitrary precision!
|
||||||
|
const y = 3.14; // type is comptime_float
|
||||||
|
```
|
||||||
|
|
||||||
|
`comptime_int` is not a runtime type. It's a big-integer that only exists at compile time. It will coerce to any concrete integer type as long as the value fits:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const big = 1_000_000_000_000; // comptime_int, no overflow
|
||||||
|
const a: u32 = big; // ERROR: value doesn't fit in u32
|
||||||
|
const b: u64 = big; // OK: fits in u64
|
||||||
|
const c: i64 = big; // OK: fits in i64
|
||||||
|
```
|
||||||
|
|
||||||
|
This is why you can write `var x: u32 = 0;` without worrying about `0` being the "wrong integer type" — `0` is `comptime_int` and coerces freely.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// These are all fine — comptime_int coerces to the annotated type
|
||||||
|
var a: u8 = 0;
|
||||||
|
var b: u32 = 0;
|
||||||
|
var c: i64 = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.4 Explicit Casting
|
||||||
|
|
||||||
|
Since nothing is implicit, Zig gives you precise casting builtins:
|
||||||
|
|
||||||
|
### `@intCast` — between integer types (must fit)
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const big: u32 = 300;
|
||||||
|
const small: u8 = @intCast(big); // runtime panic if > 255 in Debug/ReleaseSafe
|
||||||
|
// silent truncation in ReleaseFast
|
||||||
|
```
|
||||||
|
|
||||||
|
In Debug mode, `@intCast` validates the value fits. This is your safety net. In embedded ReleaseFast builds, it's a no-op cast — you're responsible.
|
||||||
|
|
||||||
|
### `@truncate` — deliberately cut bits
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const wide: u32 = 0xDEADBEEF;
|
||||||
|
const low: u8 = @truncate(wide); // 0xEF — no panic, explicit truncation
|
||||||
|
const nibble: u4 = @truncate(wide); // 0xF
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `@truncate` when you *want* the lower bits. Use `@intCast` when you *expect* the value to fit.
|
||||||
|
|
||||||
|
### `@as` — reinterpret/coerce the type
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x: u32 = @as(u32, 42); // explicit widening, always safe
|
||||||
|
```
|
||||||
|
|
||||||
|
`@as` is for cases where you want to be explicit about the target type without asserting anything about range. It's the "I know what I'm doing" cast for widening or same-size reinterpretation.
|
||||||
|
|
||||||
|
### `@bitCast` — reinterpret raw bits
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const f: f32 = 1.0;
|
||||||
|
const bits: u32 = @bitCast(f); // same bit pattern, different type
|
||||||
|
// bits == 0x3F800000
|
||||||
|
|
||||||
|
const back: f32 = @bitCast(bits); // round-trip
|
||||||
|
```
|
||||||
|
|
||||||
|
For embedded: this is how you inspect the raw IEEE 754 representation, pack/unpack register values, etc. Equivalent to Rust's `f32::to_bits()` / `f32::from_bits()`, but it's a general builtin.
|
||||||
|
|
||||||
|
### `@floatCast` — between float widths
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const d: f64 = 3.14159265358979;
|
||||||
|
const s: f32 = @floatCast(d); // precision loss, explicit
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@floatFromInt` / `@intFromFloat`
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const i: u32 = 42;
|
||||||
|
const f: f32 = @floatFromInt(i); // 42 → 42.0
|
||||||
|
|
||||||
|
const g: f64 = 9.7;
|
||||||
|
const j: i32 = @intFromFloat(g); // truncates toward zero → 9
|
||||||
|
```
|
||||||
|
|
||||||
|
No implicit `as f32` like Rust. Every int↔float conversion is named and explicit.
|
||||||
|
|
||||||
|
### Casting cheatsheet
|
||||||
|
|
||||||
|
| What you want | Builtin |
|
||||||
|
|---|---|
|
||||||
|
| Widen/shrink integer (value must fit) | `@intCast` |
|
||||||
|
| Truncate integer (keep low bits) | `@truncate` |
|
||||||
|
| Reinterpret bits (same size) | `@bitCast` |
|
||||||
|
| Explicit type annotation/widening | `@as` |
|
||||||
|
| Float ↔ float (different width) | `@floatCast` |
|
||||||
|
| Int → float | `@floatFromInt` |
|
||||||
|
| Float → int (truncates) | `@intFromFloat` |
|
||||||
|
| Enum ↔ int | `@intFromEnum` / `@enumFromInt` |
|
||||||
|
| Pointer ↔ int | `@intFromPtr` / `@ptrFromInt` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.5 Float Types
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const a: f16 = 1.5; // half precision (useful in ML, some embedded)
|
||||||
|
const b: f32 = 3.14; // single precision
|
||||||
|
const c: f64 = 2.71828; // double precision
|
||||||
|
const d: f80 = 1.0; // extended (x86 only)
|
||||||
|
const e: f128 = 1.0; // quad precision
|
||||||
|
```
|
||||||
|
|
||||||
|
Float literals without annotation are `comptime_float` — arbitrary precision at compile time, coerced to a concrete type when needed.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const pi = 3.141592653589793; // comptime_float, full precision
|
||||||
|
const pf: f32 = pi; // rounded to f32 precision at compile time
|
||||||
|
const pd: f64 = pi; // rounded to f64 precision at compile time
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.6 Booleans
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const a: bool = true;
|
||||||
|
const b: bool = false;
|
||||||
|
|
||||||
|
// Boolean operators
|
||||||
|
const and_result = a and b; // false — short-circuits
|
||||||
|
const or_result = a or b; // true — short-circuits
|
||||||
|
const not_result = !a; // false
|
||||||
|
|
||||||
|
// Comparison operators produce bool
|
||||||
|
const eq = (1 == 1); // true
|
||||||
|
const neq = (1 != 2); // true
|
||||||
|
const lt = (3 < 5); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
No truthiness. `if (1)` is a compile error — you must produce an actual `bool`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.7 Optionals — Zig's Nullable Types
|
||||||
|
|
||||||
|
Zig has no null pointers by default. To express "this might not have a value", you use an **optional type**: `?T`.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const maybe: ?u32 = null;
|
||||||
|
const value: ?u32 = 42;
|
||||||
|
```
|
||||||
|
|
||||||
|
This maps directly to Rust's `Option<T>`. The syntax difference:
|
||||||
|
|
||||||
|
| Rust | Zig |
|
||||||
|
|---|---|
|
||||||
|
| `Option<u32>` | `?u32` |
|
||||||
|
| `Some(42)` | `42` (coerces automatically) |
|
||||||
|
| `None` | `null` |
|
||||||
|
| `x.unwrap()` | `x.?` |
|
||||||
|
| `if let Some(v) = x` | `if (x) \|v\|` |
|
||||||
|
| `x.unwrap_or(0)` | `x orelse 0` |
|
||||||
|
|
||||||
|
### Unwrapping optionals
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const maybe: ?u32 = 42;
|
||||||
|
|
||||||
|
// Method 1: .? — panics if null (like Rust's .unwrap())
|
||||||
|
const val = maybe.?;
|
||||||
|
|
||||||
|
// Method 2: orelse — provide a default (like Rust's .unwrap_or())
|
||||||
|
const safe = maybe orelse 0;
|
||||||
|
|
||||||
|
// Method 3: orelse with a block (like Rust's .unwrap_or_else())
|
||||||
|
const computed = maybe orelse blk: {
|
||||||
|
// do work...
|
||||||
|
break :blk 99;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method 4: if capture — safe unwrap (like Rust's if let Some(v) = ...)
|
||||||
|
if (maybe) |v| {
|
||||||
|
std.debug.print("got: {d}\n", .{v});
|
||||||
|
} else {
|
||||||
|
std.debug.print("was null\n", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 5: while capture — iterate until null
|
||||||
|
var cursor: ?u32 = 0;
|
||||||
|
while (cursor) |c| {
|
||||||
|
std.debug.print("{d}\n", .{c});
|
||||||
|
cursor = if (c < 5) c + 1 else null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional pointers are free
|
||||||
|
|
||||||
|
A `?*T` in Zig has the same size as `*T` — null is represented as address zero. No extra byte wasted.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const ptr: ?*u32 = null;
|
||||||
|
std.debug.print("size of ?*u32: {d}\n", .{@sizeOf(?*u32)});
|
||||||
|
// Same as @sizeOf(*u32) — zero cost
|
||||||
|
```
|
||||||
|
|
||||||
|
This is identical to how `Option<&T>` is optimized in Rust using the null pointer optimization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.8 The `undefined` Value
|
||||||
|
|
||||||
|
`undefined` is a special value you can assign to any type to declare it without initializing:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
var buf: [256]u8 = undefined; // memory is allocated, not zeroed
|
||||||
|
var x: u32 = undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
In **Debug** builds, Zig writes `0xAA` into all `undefined` memory. This deliberately makes bugs visible — a read of uninitialized memory produces an obviously wrong value rather than a lucky zero.
|
||||||
|
|
||||||
|
In **ReleaseFast**, `undefined` means whatever bits were already there. Using an undefined value is **undefined behavior**.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Pattern: declare undefined, then initialize before use
|
||||||
|
var result: u32 = undefined;
|
||||||
|
result = computeSomething(); // now it's valid
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the common pattern for output parameters and large stack buffers. Don't use `undefined` and then immediately read — initialize before any read path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.9 Type Reflection with `@TypeOf` and `@typeInfo`
|
||||||
|
|
||||||
|
Zig lets you inspect types at compile time. This is the foundation of comptime generics (Chapter 08), but even now it's useful for understanding what types you actually have.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = 42;
|
||||||
|
const T = @TypeOf(x); // comptime_int
|
||||||
|
std.debug.print("type: {s}\n", .{@typeName(T)}); // prints "comptime_int"
|
||||||
|
|
||||||
|
const y: u32 = 42;
|
||||||
|
const U = @TypeOf(y); // u32
|
||||||
|
std.debug.print("type: {s}\n", .{@typeName(U)}); // prints "u32"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@sizeOf` and `@bitSizeOf`
|
||||||
|
|
||||||
|
Critical for embedded work:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
std.debug.print("u8 = {d} bytes\n", .{@sizeOf(u8)}); // 1
|
||||||
|
std.debug.print("u32 = {d} bytes\n", .{@sizeOf(u32)}); // 4
|
||||||
|
std.debug.print("u3 = {d} bytes\n", .{@sizeOf(u3)}); // 1 (rounded up)
|
||||||
|
std.debug.print("u3 = {d} bits\n", .{@bitSizeOf(u3)}); // 3
|
||||||
|
|
||||||
|
// In packed structs (Chapter 06), u3 actually takes exactly 3 bits
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@alignOf`
|
||||||
|
|
||||||
|
```zig
|
||||||
|
std.debug.print("u32 align = {d}\n", .{@alignOf(u32)}); // 4
|
||||||
|
std.debug.print("u8 align = {d}\n", .{@alignOf(u8)}); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.10 Type Aliases and Named Types
|
||||||
|
|
||||||
|
In Zig, types are values. A type alias is just a `const` that holds a type:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const MyU32 = u32; // alias
|
||||||
|
const Count = usize; // semantic alias
|
||||||
|
|
||||||
|
var x: MyU32 = 42;
|
||||||
|
var n: Count = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
But these are *true aliases* — `MyU32` and `u32` are the same type, completely interchangeable.
|
||||||
|
|
||||||
|
For distinct types (like Rust's newtype pattern), you use a struct:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
// Rust newtype: struct Meters(f32);
|
||||||
|
// Zig equivalent:
|
||||||
|
const Meters = struct { value: f32 };
|
||||||
|
const Seconds = struct { value: f32 };
|
||||||
|
|
||||||
|
const d = Meters{ .value = 100.0 };
|
||||||
|
const t = Seconds{ .value = 9.58 };
|
||||||
|
|
||||||
|
// d and t are incompatible types — can't mix them up
|
||||||
|
```
|
||||||
|
|
||||||
|
Full struct syntax in Chapter 06. The point here: Zig's type system is built on a simple foundation — types are comptime values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.11 Peer Type Resolution
|
||||||
|
|
||||||
|
The one place Zig does implicit coercion. When two branches produce values that must unify (in `if/else`, `switch`, etc.), Zig tries to find a common type:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x: u8 = 10;
|
||||||
|
const y: u32 = 1000;
|
||||||
|
|
||||||
|
// Peer type resolution: u8 can coerce to u32, so result is u32
|
||||||
|
const z = if (true) x else y;
|
||||||
|
// @TypeOf(z) == u32
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Integer types: the "wider" type wins if the narrower can fit.
|
||||||
|
- If neither can coerce to the other → compile error.
|
||||||
|
- Optional: `T` and `?T` → result is `?T`.
|
||||||
|
- Error union: `T` and `!T` → result is `!T`.
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const a: ?u32 = 42;
|
||||||
|
const b: u32 = 0;
|
||||||
|
const c = if (true) a else b;
|
||||||
|
// @TypeOf(c) == ?u32
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.12 Variable Shadowing
|
||||||
|
|
||||||
|
Unlike Rust, Zig **does not allow shadowing** within the same scope:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = 1;
|
||||||
|
const x = 2; // ERROR: redefinition of 'x'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can shadow in an inner scope:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = 1;
|
||||||
|
{
|
||||||
|
const x = 2; // OK — different scope
|
||||||
|
_ = x;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is stricter than Rust (where `let x = x + 1;` is common). Plan your variable names accordingly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.13 Unused Variables and `_`
|
||||||
|
|
||||||
|
Zig **refuses to compile** if you declare a variable and never use it:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = 42; // ERROR: unused local constant
|
||||||
|
```
|
||||||
|
|
||||||
|
To explicitly discard a value, assign to `_`:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const x = computeSomething();
|
||||||
|
_ = x; // "I know, I'm intentionally discarding this"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is stricter than Rust's `#[allow(unused)]`. It forces you to be deliberate. In embedded this is valuable — you can't accidentally ignore a return value.
|
||||||
|
|
||||||
|
For function parameters you don't use:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
fn callback(data: u32, _: *anyopaque) void {
|
||||||
|
// use data, ignore the second param
|
||||||
|
_ = data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or just prefix with `_`:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
fn callback(_data: u32, _ctx: *anyopaque) void {
|
||||||
|
// both discarded — prefixing with _ suppresses the warning
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exercise 1 — Cast Gauntlet
|
||||||
|
|
||||||
|
Write a function `castDemo()` that demonstrates each of the following, printing the result each time:
|
||||||
|
|
||||||
|
1. A `u32` value of `300` cast to `u8` using `@truncate` — print the result and explain in a comment why it's not 300.
|
||||||
|
2. A `u8` value of `200` widened to `u32` using `@intCast` — print the size of the result type using `@sizeOf(@TypeOf(result))`.
|
||||||
|
3. An `f32` of `3.99` converted to `i32` using `@intFromFloat` — print the result and note the truncation direction.
|
||||||
|
4. A `u32` bit-cast to `f32` using `@bitCast` — use the value `0x3F800000` and verify the result is `1.0`.
|
||||||
|
5. A `f64` value narrowed to `f32` using `@floatCast`.
|
||||||
|
|
||||||
|
No panics should occur when run in Debug mode — choose your values carefully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exercise 2 — Optional Chain
|
||||||
|
|
||||||
|
Model a simple sensor read pipeline using optionals. Write three functions:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
fn readRaw() ?u16 // returns null 30% of the time (use a counter/flag, not random)
|
||||||
|
fn validate(raw: u16) ?u16 // returns null if raw > 4000 (out of range)
|
||||||
|
fn toMillivolts(raw: u16) u32 // converts: millivolts = raw * 3300 / 4095
|
||||||
|
```
|
||||||
|
|
||||||
|
In `main`, call them chained using `orelse` and `if` captures. Print either the millivolt result or a descriptive error message indicating which stage failed.
|
||||||
|
|
||||||
|
**Bonus:** Rewrite the chain using a single `if` with nested captures:
|
||||||
|
```zig
|
||||||
|
if (readRaw()) |raw| {
|
||||||
|
if (validate(raw)) |valid| { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exercise 3 — Arbitrary Width Integers
|
||||||
|
|
||||||
|
You're modeling a hardware register for a made-up microcontroller. The register is 16 bits wide with this layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
Bits 15-12: MODE (4 bits, unsigned)
|
||||||
|
Bits 11-8: SPEED (4 bits, unsigned)
|
||||||
|
Bits 7-4: FLAGS (4 bits, unsigned)
|
||||||
|
Bits 3-0: ADDRESS (4 bits, unsigned)
|
||||||
|
```
|
||||||
|
|
||||||
|
Using only `u4` fields and bitwise operations on `u16`:
|
||||||
|
|
||||||
|
1. Declare four `u4` constants: `mode = 0b1010`, `speed = 0b0011`, `flags = 0b1100`, `address = 0b0101`.
|
||||||
|
2. Pack them into a single `u16` using shift (`<<`) and bitwise OR (`|`).
|
||||||
|
3. Print the packed value in hex.
|
||||||
|
4. Unpack the `u16` back into four `u4` values using shift (`>>`) and `@truncate`.
|
||||||
|
5. Verify (with `std.debug.assert`) all four unpacked values match the originals.
|
||||||
|
|
||||||
|
*(Packed structs in Chapter 06 will make this much cleaner — this exercise shows you why they exist.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exercise 4 — Type Inspector
|
||||||
|
|
||||||
|
Write a function `inspect(comptime T: type) void` that prints:
|
||||||
|
- The name of the type (`@typeName`)
|
||||||
|
- Its size in bytes (`@sizeOf`)
|
||||||
|
- Its size in bits (`@bitSizeOf`)
|
||||||
|
- Its alignment (`@alignOf`)
|
||||||
|
|
||||||
|
Call it with: `u8`, `u32`, `u3`, `f64`, `bool`, `?u32`, `*u8`.
|
||||||
|
|
||||||
|
Observe the output for `u3` (size rounds up to 1 byte but bitSize is 3) and `?u32` (size is 8 bytes — why?).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exercise 5 — Newtype Pattern
|
||||||
|
|
||||||
|
Implement a minimal newtype pattern for a distance calculation:
|
||||||
|
|
||||||
|
1. Define `Meters` and `Kilometers` as single-field structs wrapping `f32`.
|
||||||
|
2. Write `fn toKilometers(m: Meters) Kilometers`.
|
||||||
|
3. Write `fn toMeters(km: Kilometers) Meters`.
|
||||||
|
4. Write `fn addMeters(a: Meters, b: Meters) Meters`.
|
||||||
|
5. In `main`, create some values, convert between them, add them, and print results.
|
||||||
|
6. Try to accidentally add a `Meters` and a `Kilometers` directly — observe the compile error, then delete that line.
|
||||||
|
|
||||||
|
The goal is to see how Zig enforces type safety through structs even without a dedicated newtype syntax.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
By the end of this chapter you should be able to:
|
||||||
|
|
||||||
|
- [ ] Explain why Zig has no implicit integer coercion and what `comptime_int` is
|
||||||
|
- [ ] Choose the right cast builtin for any integer/float conversion
|
||||||
|
- [ ] Use arbitrary-width integers like `u4`, `u12` for embedded register modeling
|
||||||
|
- [ ] Declare and unwrap optional types using `orelse`, `.?`, and `if` capture
|
||||||
|
- [ ] Use `undefined` safely — declare early, initialize before read
|
||||||
|
- [ ] Inspect types at compile time using `@TypeOf`, `@sizeOf`, `@alignOf`, `@typeName`
|
||||||
|
- [ ] Implement the newtype pattern using single-field structs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next:** Chapter 03 — Control Flow — deep dive into `switch` with exhaustive matching, labeled blocks that return values, `inline for`, and `comptime` branches.
|
||||||
Reference in New Issue
Block a user