Hedronite Lesson · Polyglot-Dev / V · Tue 2026-10-06 · V session 2 of 5

Go-shaped rules: immutable by default, option/result, bounds checks, no globals · Mark the Mut · Gate the Option · Let the Index Panic · Fence the Globals

Mark the mut. Gate the option. Let the index panic. Fence the globals.

Lesson Class: Asr (V Lang bridge · learn-first · watch-not-adopt)
Focus: Mark the Mut · Gate the Option · Let the Index Panic · Fence the Globals
Done-criteria: declare and mutate with mut; one !T fn handled with or {} and with ! propagation; one ?T; out-of-range index behavior and @[direct_array_access]; what -enable-globals is for
Grounding: vlang/v 0.5.2 (7647ce1) docs.md + vlib/builtin/array.v + help/build.txt + parser/checker · vlang.io/compare live for claim text
Note: The lab Mac (macOS arm64) · V 0.5.2 7647ce1 from Duha #6 by absolute path · 22 programs, real logs in lab/
Mark the Mut
mut on the binding, on the parameter, and at the call. Primitives are returned, not mutated. -disable-explicit-mutability exists.
Gate the Option
!T fails at a bare call; ?T fails at first use. or {}, ! propagation, if-unwrap. ! in main panics.
Let the Index Panic
arr[5] panics via array.get. @[direct_array_access] and -no-bounds-checking emit raw C indexing; -force-bounds-checking puts the check back.
Mark the mut. Gate every option. Let the index panic. Close the editor.

<!-- hal:authoritative:yaml -->

Mark the mut. Gate the option. Let the index panic. Fence the globals.

§I. Frame

Asr V session 2 of 5, Tue 2026-10-06. Learn-first, contributor-aware, watch-not-adopt. Cites are vlang/v at tag 0.5.2 (7647ce1) only: doc/docs.md and source files at the tag. The lab ran on the lab Mac with the compiler Duha #6 built this morning (~/src/vlang-duha6/v/v, called by absolute path). Every error string below is copied from lab/lab.log and was found again in the 0.5.2 source.

The compare page says V improves on Go with "no err != nil checks", "immutability by default", "no global state", and "no uninitialized variables". Those are author claims. This session tests each one against the compiler.

§II. Four techniques

  1. Mark the Mut. A binding changes only if it was declared mut, and a function changes an argument only if both the signature and the call say mut.
  2. Gate the Option. A ?T or !T value never reaches code that wants a plain T until it passes an or {} block, a ! propagation, or an if x := ... unwrap.
  3. Let the Index Panic. Array indexing is checked at runtime by default. You opt out per function with @[direct_array_access] or per build with -no-bounds-checking, and you pay in undefined behavior if you are wrong.
  4. Fence the Globals. Variables live inside functions. __global exists, behind -enable-globals, for kernels and drivers.

§III. Mark the Mut

docs.md: := "is the only way to declare variables in V", so every variable has an initial value, and variables "are immutable by default". Three programs, three refusals:

Arguments follow the same rule with one twist. The callee declares fn multiply_by_2(mut arr []int), and the caller must write multiply_by_2(mut nums). Leaving mut off the call (06) gives parameter `arr` is `mut`, so use `mut nums` instead. Writing into an argument the signature did not mark (07) gives the same "is immutable" error as a local.

docs.md also says V "doesn't allow the modification of arguments with primitive types". The compiler agrees, and the message tells you what to do instead (08): "mutable arguments are only allowed for arrays, interfaces, maps, pointers, structs or their aliases", then return values instead: `fn foo(mut n int) {` => `fn foo(n int) int {`.

One flag cuts against the headline. v help build at 0.5.2 lists -disable-explicit-mutability, which lets "ordinary variables and mutable call arguments in user code" skip mut. With it, the program that failed in 02 printed 21 and exited 0. Immutability by default is a compiler default, and the binary carries a switch to turn it off. docs.md does not mention the switch.

§IV. Gate the Option

docs.md: ?Type holds a value or none; !Type holds a value or an error. The lab's 09_result_option.v exercised all four handling methods the docs list:

  1. Default value: parse_port('nope') or { 3000 } gave 3000.
  2. Inspect and recover: inside or { } the name err printed bad port: "70000".
  3. Propagate: load() calls parse_port(s)!. A good port came back as 8081. A bad one surfaced in the caller's or block as bad port: "0".
  4. Unwrap with if: if port := find_env(env, 'PORT') printed PORT=8080; the HOME lookup fell to else as none.

What happens when you skip the gate. An unhandled Result fails at the call (10): parse_port() returns `!int`, so it should have either an `or {}` block, or `!` at the end. An unhandled Option behaves differently in 0.5.2 (11): e := first_even(...) compiled, and the error landed one line later where e + 1 used it, ?int` cannot be used as `int literal`, unwrap the option first. So an Option can sit in a variable as an Option; a Result cannot be left bare. Both stop the build before a T is used.

! inside main has nowhere to propagate; docs.md says it will "panic instead", and 13 printed V panic: result not set (bad port: "zero"), exit 1. There is no .unwrap(); docs.md says to write or { panic(err) }.

"No null" is a compare-page claim. In user code p := nil fails with error: `nil` is only allowed in `unsafe` code (12). docs.md also shows unsafe { nil } reference fields and says nil references "will NOT be supported in the future". The precise statement: nil exists, behind unsafe.

§V. Let the Index Panic

The syllabus left this cell as verify-at-fire. Answer from 14_bounds_panic.v: indexing arr[5] on a three-element array printed before, then V panic: array.get: index out of range (i,a.len):5, 3 and a backtrace, exit 1. after never printed. The panic comes from fn (a array) get in vlib/builtin/array.v, inside a $if !no_bounds_checking guard.

The Maps section of docs.md gives arrays the same option gate as maps. arr[5] or { -1 } returned -1, and arr[i]! inside a !int function propagated array index out of range to the caller (15). Maps differ: a missing key returns the zero value (0) unless you add or {}.

Opting out, shown in generated C (16). In a normal function a[i] compiled to builtin__array_get(a, i). Under @[direct_array_access] the same line became ((int*)a.data)[i], a raw C index. docs.md calls that "unsafe" unless you checked the bounds yourself, and lists "Everywhere else" under When to Avoid. Build-wide, -no-bounds-checking produced the raw index in main too. Its help text says the check usually costs "below 3%"; that figure is the authors' and we did not measure it. -force-bounds-checking puts the check back even inside tagged functions: 22_daa_oob.v then panicked at index 7. Without the flag that program reads past the array in C, which is undefined behavior, so the lab did not run it.

§VI. Fence the Globals, name things, run fmt

__global ( counter int ) without the flag (17): error: use `v -enable-globals ...` to enable globals. With -enable-globals it ran and printed 2. docs.md says the flag is "intended for low-level applications like kernels and drivers" and warns that global access in threaded code "is subject to race conditions".

A top-level count := 0 (18) does not produce a globals error. V reads it as a script statement and then refuses the fn main after it: "all definitions must occur before code in script mode". Asr #1 met that rule from the other side.

Naming is enforced, not advised. fn DoThing() (19): "function names cannot contain uppercase letters, use snake_case instead". An unused local is a warning in dev builds (21 still ran); docs.md says -prod refuses it. Asr #5 measures -prod.

v fmt -verify exited 1 on a scrambled file, v fmt -w rewrote it, and a second verify exited 0. Running verify over the other lab files flagged 04, 08, and 19 as formatting errors, which looked wrong until the output showed why. Those three rules (shadowing, mut primitives, uppercase function names) are enforced in the parser (vlib/v/parser/assign.v, fn.v), and vfmt parses before it formats. The immutability and option errors live in vlib/v/checker/checker.v and do not stop vfmt.

§VII. Contributor note

docs.md code fences carry tags such as failcompile, oksyntax, nofmt, ignore, and globals. cmd/tools/vcheck-md.v reads them: a failcompile block that compiles is an error. CONTRIBUTING at 0.5.2 asks for v check-md on any changed Markdown. Today's run found three gaps a learner could draft into a docs change:

  1. Arrays never states the default out-of-range behavior. It shows only the or {} form, under Maps.
  2. -no-bounds-checking, -force-bounds-checking, and -disable-explicit-mutability appear in v help build and not in docs.md.
  3. Mutable variables says "Try compiling the program above after removing mut" without showing the result. A failcompile block would make that sentence tested.

Duha #7 and #8 own the fork, v fmt -w, v check-md, and draft-PR steps. Nothing goes upstream from a lesson.

§VIII. Lab

  1. Predict the exit codes of 02, 09, 10, and 14, then run them with the pinned compiler by absolute path.
  2. Emit C for 16 and compare the two sum_ bodies.
  3. Run v fmt -verify on 04 and explain why a semantic rule shows up as a formatting error.

Evidence: lab/lab.sh, lab/lab.log, and the 22 .v files beside them.

§IX. Common mistakes

  1. Writing mut on the parameter and forgetting it at the call, or reaching for mut n int instead of returning the value.
  2. Expecting a bare ?T assignment to fail at the call, the way !T does.
  3. Using ! in main and calling it error handling. It is a panic.
  4. Adding @[direct_array_access] before measuring anything.
  5. Reading "no null" and "immutable by default" as absolutes. unsafe { nil } and -disable-explicit-mutability both exist.

§X. V next to Go and Rust, session 2

Next to Go, the four compare-page claims held in the 0.5.2 compiler, and two of them (immutability, no globals) come with a flag that switches them off. Next to Rust, ?T/!T with or {} maps onto Option/Result with ? and unwrap_or, minus a forced unwrap; V errors default to a message string, with a custom IError for typed ones. Rust has no flag that turns off mut; V 0.5.2 ships one. Both panic on a bad index by default and both offer an unchecked path. Verdict: the rules are real and readable, and the escape hatches live in help text more than in docs. Learn it, adopt nothing. Asr #3 takes structs, interfaces, sum types, and modules tomorrow.

Related:

Mark the mut. Gate every option. Let the index panic. Close the editor.