12. Pointers, references and memory

Level: beginner · Reading time: 20 min · Prerequisite: book/chapters/11-collections.html · Track: essential · Maturity: reviewed · Last review: 2026-05-09

TL;DR (5 lines)

  • Low-level surfaces need narrow boundaries.
  • The chapter should explain risk and ownership first.
  • Unsafe examples should stay explicit.
  • The reader should learn where the boundary belongs.
  • Reviewability matters more than token novelty.

Frequent mistakes

  • Treating low-level code as ordinary glue with no special boundary.
  • Explaining syntax before explaining risk.
  • Failing to isolate the low-level edge from the rest of the program.

Prerequisites: book/chapters/11-collections.html. See also: book/chapters/11-collections.html, book/chapters/27-grammar.html, book/chapters/31-build-errors.html.

Concrete Problem

Low-level chapters become dangerous when they show syntax without ownership, lifetime, or boundary reasoning.

Red Thread (Single Project)

One explicit pointer-facing helper isolates a low-level boundary while keeping the rest of the flow simple.

For what

This chapter helps the reader understand why pointer-like surfaces should remain explicit and narrow.

Work in this chapter

You will inspect one narrow low-level helper, then compare it to an unsafe misuse.

Coherent example

space demo/pointers

unsafe proc write_value(dst: ptr[int], value: int) -> int {
  give 0
}

export *

Complete examples

Each block below is a complete reading unit with its own boundary, data shape, and observable result.

Primary coherent example

This is the compact chapter anchor used by the surrounding explanation.

space demo/pointers

unsafe proc write_value(dst: ptr[int], value: int) -> int {
  give 0
}

export *

Narrow unsafe helper

This keeps the unsafe operation behind one named boundary.

space examples/pointers/narrow

unsafe proc store_value(dst: ptr[int], value: int) -> int {
  give 0
}

proc write_checked(dst: ptr[int], value: int) -> int {
  if value < 0 { give 11 }
  give store_value(dst, value)
}

export *

Safe caller surface

This wrapper keeps low-level detail outside normal application flow.

space examples/pointers/wrapper

unsafe proc host_write(code: int) -> int {
  give code
}

proc write_status(code: int) -> int {
  if code < 0 { give 12 }
  give host_write(code)
}

proc main(args: list[string]) -> int {
  give write_status(0)
}

export *

Immediate work for this chapter

  • Show a safe caller, an explicit unsafe edge, and a checked result.
  • Keep raw pointers out of broad public APIs.
  • Name the review risks: aliasing, lifetime, null, escape, and mutation.
  • Use interop or runtime boundaries as the justification for unsafe code.
  • Keep the first code block complete and aligned with current Vitte docs syntax.
  • Keep the invalid block focused on one broken contract only.
  • Replace generic prose with one concrete rule visible in the code.
  • Keep every example small enough to review from top to bottom.

Chapter override: 12-pointers.html

Dedicated problem

This chapter must teach raw memory as an explicitly reviewed boundary. The reader should leave with a concrete distinction between ordinary safe callers, the smallest unsafe edge, and the checks that keep raw pointer usage from spreading through a module.

Specific complete examples

Checked write wrapper

A normal caller validates domain input before invoking the unsafe pointer-facing operation.

space examples/pointers/checked_write

unsafe proc raw_write(dst: ptr[int], value: int) -> int {
  give 0
}

proc write_checked(dst: ptr[int], value: int) -> int {
  if value < 0 { give 11 }
  give raw_write(dst, value)
}

proc main(args: list[string]) -> int {
  let status: int = write_checked(0, 7)
  give status
}

export *

Read-only borrow boundary

A read-facing helper returns an ordinary value and keeps pointer risk out of callers.

space examples/pointers/read_only

unsafe proc raw_read(src: ptr[int]) -> int {
  give 0
}

proc read_status(src: ptr[int]) -> int {
  let value: int = raw_read(src)
  if value < 0 { give 12 }
  give value
}

proc main(args: list[string]) -> int {
  give read_status(0)
}

export *

Mutation status boundary

Mutation stays behind a narrow status-returning surface instead of escaping into public API.

space examples/pointers/mutation_status

unsafe proc raw_increment(slot: ptr[int]) -> int {
  give 0
}

proc increment_checked(slot: ptr[int], enabled: bool) -> int {
  if not enabled { give 20 }
  give raw_increment(slot)
}

proc main(args: list[string]) -> int {
  give increment_checked(0, true)
}

export *

Risks and diagnostics

RiskDiagnostic signalAction
AliasingTwo mutable names can affect the same storage.Keep mutation behind one wrapper and one status result.
LifetimeThe pointer can outlive the value it targets.Do not store raw pointers in broad public data shapes.
Null or dangling pointerThe pointer value does not identify valid storage.Validate at the wrapper boundary and return an error code.
Unsafe escapeRaw memory assumptions leak into ordinary callers.Expose safe procedures, not raw pointer operations.

Review checklist

  • Unsafe code is local, named, and grep-friendly.
  • Every unsafe procedure has a safe caller or wrapper beside it.
  • The public API returns ordinary Vitte values or status codes.
  • Aliasing, lifetime, null, escape, and mutation are mentioned near the example.
  • The invalid case breaks exactly one pointer contract.

Production use

  • Use this pattern for interop, runtime buffers, allocator internals, and constrained device memory.
  • Require review on every change that widens the raw pointer surface.
  • Pin regression fixtures around unsafe wrappers rather than around every internal instruction.

What to avoid

  • Do not use pointers to bypass a type contract that can be modeled as a form or pick.
  • Do not let raw pointers appear in project-level service APIs.
  • Do not treat unsafe code as an optimization until measurement identifies the memory boundary.

Global explanation

Pointer chapters are about boundaries. The goal is not to normalize low-level code everywhere, but to show how a narrow low-level surface can remain explicit and reviewable.

Invalid case

proc bad_ptr() -> int {
  let dst: ptr[int] = 0
  give dst
}

This invalid case is intentionally small. It exists to isolate the contract failure that the chapter is trying to teach.

Common pitfalls

  • Treating low-level code as ordinary glue with no special boundary.
  • Explaining syntax before explaining risk.
  • Failing to isolate the low-level edge from the rest of the program.

Short exercise

Wrap the low-level helper behind a higher-level procedure that exposes a safer contract.

Summary in 5 points

  1. Low-level surfaces need narrow boundaries.
  2. The chapter should explain risk and ownership first.
  3. Unsafe examples should stay explicit.
  4. The reader should learn where the boundary belongs.
  5. Reviewability matters more than token novelty.

See also

Next best action

Extend the coherent example by one small, justified step and keep the same contract visible from input to output.

Chapter deep dive

12. Pointers, references and memory keeps low-level power narrow and reviewable. The chapter is written for a reader deciding when a low-level surface is justified.

The practical boundary is: safe caller, explicit unsafe edge, and checked result. Keep that boundary in view while reading the example, the invalid case, and the exercise.

Role in the learning path

Low-level chapters become dangerous when they show syntax without ownership, lifetime, or boundary reasoning.

One explicit pointer-facing helper isolates a low-level boundary while keeping the rest of the flow simple.

This chapter helps the reader understand why pointer-like surfaces should remain explicit and narrow.

Profile-specific deep dive

Safe wrapper

  • The normal caller should see a checked procedure returning an ordinary status code.
  • The raw pointer should appear only in the smallest procedure that needs it.
  • The wrapper must validate obvious preconditions before crossing the unsafe edge.

Unsafe edge

  • `unsafe proc` marks the review boundary, not a permission to spread raw memory through the module.
  • Interop, runtime storage, and controlled buffer operations are valid reasons to expose the edge.
  • The low-level operation should return a status instead of leaking raw pointer state.

Memory risks

  • Aliasing risk appears when two names can mutate the same storage.
  • Lifetime risk appears when a pointer can outlive the value it points to.
  • Null, dangling, escape, and shared mutation risks must be named near the example.

Review checklist

  • Unsafe code is local and easy to grep.
  • Raw pointers do not cross the broad public API.
  • The safe caller checks inputs and validates the returned status.

Reading the valid example

  1. space demo/pointers: names the ownership boundary before any behavior appears.
  2. unsafe proc write_value(dst: ptr[int], value: int) -> int {: supports the chapter contract without adding hidden behavior.
  3. give 0: ends the local path with an explicit result.
  4. }: supports the chapter contract without adding hidden behavior.
  5. export *: supports the chapter contract without adding hidden behavior.

Lesson from the invalid example

  1. proc bad_ptr() -> int {: this line helps isolate the failure because it states the callable contract: inputs first, result shape last.
  2. let dst: ptr[int] = 0: this line helps isolate the failure because it keeps an intermediate decision visible for review.
  3. give dst: this line helps isolate the failure because it ends the local path with an explicit result.
  4. }: this line helps isolate the failure because it supports the chapter contract without adding hidden behavior.

Engineering decisions to preserve

  • Name the boundary before changing code: Low-level surfaces need narrow boundaries.
  • Keep the smallest example executable: The chapter should explain risk and ownership first.
  • Make the invalid path explain one failure only: Unsafe examples should stay explicit.
  • Prefer a visible contract over an implied convention: The reader should learn where the boundary belongs.
  • Leave a review anchor that another maintainer can verify: Reviewability matters more than token novelty.

Context-specific review criteria

  • The page makes the safe caller, explicit unsafe edge, and checked result boundary visible before the first code block.
  • The intended reader, a reader deciding when a low-level surface is justified, can follow the valid example through named contracts instead of memorized tokens.
  • The invalid example fails for the same reason the prose discusses.
  • The exercise extends the same contract instead of introducing an unrelated concept.
  • The next chapter can reuse the vocabulary introduced here without redefining it.
  • The chapter stays specific enough that its title materially changes the meaning of the page.
  • Every warning connects to a concrete code shape.
  • The summary leaves one durable engineering rule behind.

Contract matrix

ConcernChapter ruleEvidence to keep
OwnershipCode belongs behind the boundary named by the chapter.The chapter keeps ownership visible through safe caller, explicit unsafe edge, and checked result.
Input contractThe procedure receives a shape that is named before branching.The valid example names the accepted shape before branching.
Nominal pathThe clean path remains readable without hidden state.The successful result can be found without reading hidden state.
Failure pathThe invalid case isolates one failure reason.The broken example has one main reason to fail.
NamingNames explain the domain rather than only the mechanism.Names remain tied to the chapter goal.
TypesTypes remove ambiguity from values and results.Fields and return values carry domain meaning.
Control flowBranches stay traceable from guard to result.Guards appear before the result they protect.
Module boundaryThe public surface stays smaller than implementation detail.The public surface remains smaller than the implementation detail.
Diagnostic valueThe failure path points back to the exact contract.The invalid example points back to the exact contract.
Test valueRegression evidence covers one passing path and one failing path.One passing case and one failing case cover the lesson.
Refactor valueImplementation cleanup preserves the result shape.The result shape stays stable during local cleanup.
Publication valueThe chapter leaves one concrete engineering rule.The chapter leaves one concrete engineering rule.

Rewrite path for this chapter

  1. Rewrite the opening paragraph so it names safe caller, explicit unsafe edge, and checked result before naming syntax.
  2. Keep the valid example small enough that the full contract fits on screen.
  3. Move any broad claim back to a specific line in the example.
  4. Preserve one invalid case that fails for the chapter's main reason.
  5. Add one sentence explaining why the invalid case is not a random error.
  6. Make every pitfall actionable by naming the code shape it damages.
  7. Keep the exercise inside the same domain as the example.
  8. Avoid introducing a second unrelated project just to show variety.
  9. Use the summary to restate the chapter rule, not the table of contents.
  10. Check that the next chapter can build on this vocabulary.
  11. Remove any sentence that would still be true in every other chapter.
  12. Keep the last action small, local, and testable.

Diagnostic anchors

  • The first inspected line is the one that declares the chapter's main contract.
  • The central type, field, procedure, or branch carries the chapter's main idea.
  • The invalid example includes a sentence-level explanation of its failure.
  • Refactors preserve the detail that would otherwise mislead a future reader.
  • The behavior that must stay stable is named before implementation changes begin.
  • Vague names are replaced before they become review friction.
  • Regression coverage protects the chapter's main contract.
  • Implementation details stay out of public API unless the chapter explicitly teaches that surface.
  • Beginner-facing diagnostics point to the contract, not to a random syntax detail.
  • The next chapter can assume one clearly named concept from this page.

When extending this chapter

  • Extend toward a reader deciding when a low-level surface is justified, not toward a broader catalog of features.
  • Add a second example only if it sharpens the same contract.
  • Prefer a small variant over a new subsystem.
  • Keep prose close to code; every abstract claim should point to a visible shape.
  • Do not hide a new concept in the exercise.
  • If a paragraph explains policy, add the concrete code boundary it protects.
  • If a paragraph explains syntax, add the semantic reason the syntax matters.
  • If a paragraph explains architecture, identify the owner of each boundary.
  • If a paragraph explains failure, keep the failing line close to the explanation.
  • Stop expanding when the chapter has one complete, testable lesson.

Failure modes to avoid

  • Treating low-level code as ordinary glue with no special boundary.
  • Explaining syntax before explaining risk.
  • Failing to isolate the low-level edge from the rest of the program.

Practice scenario

Start from the coherent example in 12. Pointers, references and memory. Change one identifier, one guard, and one returned value. After each change, write down whether the public contract is still the same contract or a new one.

If the contract changed, update the type or result shape first. If only the implementation changed, keep the external name stable and add one regression note explaining what should not change again.

Before moving on

  • You can state the chapter role: keeps low-level power narrow and reviewable.
  • You can point to the main boundary: safe caller, explicit unsafe edge, and checked result.
  • You can connect the invalid case to the problem statement: Low-level chapters become dangerous when they show syntax without ownership, lifetime, or boundary reasoning.
  • You can perform the exercise: Wrap the low-level helper behind a higher-level procedure that exposes a safer contract.