Slices, Ranges, and Visibility

When we implement the memory module, three Rust features come up together: constants for the fixed addresses, slices for passing variable-length data, and visibility rules to protect the internal state. They are small on their own, but together they form the backbone of how the memory API is written.

Constants

The memory layout has two addresses that need names — using raw numbers would make the code unreadable:

#![allow(unused)]
fn main() {
pub const MEMORY_SIZE: usize = 4096;
pub const PROGRAM_START: u16 = 0x200;
}

Constants are evaluated at compile time and inlined wherever they appear. Unlike let, they require an explicit type annotation and can be declared at module level, outside any function. The type usize is the right choice for anything used as an array length or index — it is the platform-native integer for memory sizes.

Slices: working with variable-length data

Here is the signature for loading a ROM:

#![allow(unused)]
fn main() {
pub fn load_rom(&mut self, data: &[u8]) { ... }
}

The parameter type &[u8] is a slice — a reference to a contiguous sequence of bytes whose length is not known at compile time. It can point to 10 bytes or 10,000 bytes — the function does not care.

This is different from a fixed-size array. A fixed array's size is part of its type:

TypeSize at compile time?Use case
[u8; 16]YesFixed hardware (registers, stack)
&[u8]NoROM data, variable-length input

Internally, a slice is a "fat pointer" — it carries both a pointer to the first element and the total length. You never need to pass the length separately; it is always available through .len().

Range slicing: [start..end]

To load the ROM into memory at address 0x200, we create a slice of the destination range and copy into it:

#![allow(unused)]
fn main() {
let start = PROGRAM_START as usize;
self.ram[start..start + data.len()].copy_from_slice(data);
}

[start..end] creates a slice that references elements from start up to (but not including) end. It is a view — nothing is copied yet. Then copy_from_slice reads from data and writes into that portion of ram.

Other range forms:

#![allow(unused)]
fn main() {
&arr[2..5]   // indices 2, 3, 4
&arr[2..]    // index 2 to the end
&arr[..5]    // index 0 to 4
&arr[..]     // the entire array as a slice
}

Visibility: controlling access

The memory struct has a public interface but a private internal state:

#![allow(unused)]
fn main() {
pub struct Memory {
    ram: [u8; MEMORY_SIZE],  // private — not accessible from outside this file
}

impl Memory {
    pub fn read(&self, address: u16) -> u8 { ... }
    pub fn write(&mut self, address: u16, value: u8) { ... }
}
}

In Rust, everything is private by default. ram cannot be touched from outside memory.rs — only read and write can. This matters because it gives us one place to add bounds checking, memory-mapped I/O, or logging later, without changing any of the code that calls these methods. Encapsulation is not just style — it is how you keep a growing codebase manageable.


In the emulator: load_rom uses &[u8] so it works equally well with an inline test array like &[0x13, 0x00] or a file read from disk. The copy_from_slice pattern places the ROM at exactly 0x200. See Memory Layout for the complete picture of what lives where in RAM.