Structs, methods, interfaces, sum types, modules · Fence the Fields · Let the Method Set Decide · Exhaust the Sum · One Folder, One Module
Fence the fields. Let the method set decide. Exhaust the sum. One folder, one module.
Fence the fields. Let the method set decide. Exhaust the sum. One folder, one module.
§I. Frame
Asr V session 3 of 5, Wed 2026-10-07. 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 (~/src/vlang-duha6/v/v, called by absolute path). Every message quoted below is copied from lab/lab.log.
Today's row has no compare-page claim to test. The claims under test are the docs' own sentences, and three of them did not match what the 0.5.2 compiler did. Those three are the contributor note in §VIII.
§II. Four techniques
- Fence the Fields. Struct fields are private and immutable unless a
mut:orpub:section says otherwise. Methods are functions with a receiver, and amutreceiver needs amutbinding at the call. - Let the Method Set Decide. A type satisfies an interface by having the fields and methods. No declaration is needed;
implementsis an optional check. - Exhaust the Sum. A sum type is a closed list of variants.
matchmust name every one or carryelse. - One Folder, One Module. A directory is a module, the prefix is one segment deep, and nothing leaves it without
pub.
§III. Fence the Fields
docs.md, Access modifiers: fields "are private and immutable by default (making structs immutable as well)", with five sections: none, mut:, pub:, pub mut:, and __global:. 01_struct_mut_field.v declares name in the default section, hits under mut:, and limit int = 3 under pub: with a default value. A fn (mut c Counter) hit() method raised hits to 3 and full() went from false to true.
Three refusals:
- Writing the default-section field on a
mutbinding (02):error: field `name` of struct `Counter` is immutable. Amutvariable does not make every field writable. - Writing a
mut:field through an immutable binding (03):error: `c` is immutable, declare it with `mut` to make it mutable. - Calling the
mutmethod on that binding (04): the same message, pointing atc.hit(). The receiver follows the argument rule from Asr #2.
@[required] on a field (05) turns an empty literal into error: field `Config.host` must be initialized. Without it, docs.md says every field is zeroed.
Embedding (06): Button embeds Size and gets its fields and its area() method. b.area() and b.Size.area() both gave 6. docs.md says you "cannot type cast between structs and embedded structs" and calls embedding a mixin, "NOT base classes". 07 passes a Button to a function that takes Size. The V checker let it through. Clang rejected the generated C: passing 'main__Button' ... to parameter of incompatible type 'main__Size', followed by V's own text, "This can be caused by invalid C interop code, C compiler flags, or a V compiler bug." The rule holds, but the check that caught it was the C compiler's.
That first run also printed Sent C compiler bug report to https://bugs.vlang.io/bug-report. and a report id. vlib/v/builder/c_error_report.v at 0.5.2 uploads a report on every C compile error unless V_C_ERROR_BUG_REPORT_DISABLED=1 is set. Its comments say the upload carries up to 40 V source lines on each side of the failing line, "deliberately not the whole file". v help build-c documents the variable; docs.md does not. The lab script now exports it, and the shipped lab.log is the second run, which sent nothing.
§IV. Let the Method Set Decide
08_interface_implicit.v follows docs.md: Dog and Cat each have a breed field and speak(), and neither names Speaker. Both went into a []Speaker. An interface method fn (s Speaker) intro() ran for both. docs.md says such methods are not default implementations; the type's own method wins only after a smart cast. Inside if item is Dog, typeof(item).name printed &Dog. The cast hands you a reference, which docs.md notes under Casting an interface.
A type missing a method (09): error: `Rock` doesn't implement method `speak` of interface `Speaker`, then a second error refusing the cast. With struct Rock implements Speaker (11) the same message lands on the struct line, before any use. That is the job of the optional keyword: the error moves to the declaration.
The mut: section of an interface did not hold. docs.md: "Implementing types will need to have a mut receiver, for methods declared in the mut: section", and its example says passing a value-receiver MyStruct to fn2(s Bar) is a compile error. 10b is that example with the commented line restored. It compiled, printed fn2 got Bar, and exited 0. 10 casts through a mut binding and also ran.
Generated C for 20_c_shapes.v shows the cost. main__Namer is a union of object pointers plus u32 _typ and void* _methods. describe(n) compiles to a call through _main__Namer_interface_methods->_method_name(n._object): one table lookup per call, built by the compiler.
§V. Exhaust the Sum
type Shape = Circle | Rect | Square (12). area() uses match s with one arm per variant, and inside each arm s is already the variant: s.r, s.w, s.side. The loop printed Circle area=3.0, Rect area=6.0, Square area=16.0 from type_name(). A recursive type Tree = Empty | Node summed to 14.
Drop the Square arm (13): error: match must be exhaustive (add match branches for: `Square` or `else {}` at the end). That error is the reason to choose a sum type over an interface for a closed set.
as is the unchecked cast. 14 holds a Moon and asks for Mars: V panic: as cast: cannot cast `main.Moon` to `main.Mars`, exit 1. docs.md says to prefer a smart cast.
docs.md also says a mutable variable keeps its original type in if w is Mars unless you write if mut w is Mars. 15 leaves the mut off, calls w.dust_storm() inside the branch, and printed true, exit 0. With mut (end of 12) it also worked.
In C, main__Shape is a union of variant pointers plus u32 _typ. A sum value points to its payload, and _typ records which variant it holds.
§VI. One Folder, One Module
16_modproj/ has v.mod, main.v, and geom/shapes.v plus geom/more.v, both starting module geom. main.v called geom.new_rect(2, 3) and printed area=6 tag=2x3. more.v called the private secret_tag() from shapes.v. docs.md: items are usable "between the files of a module regardless of whether or not they are prefaced with the pub keyword".
The first draft wrote import geom and then import geom { square }. 0.5.2 refused the second line: A module `geom` was already imported on line 3`. docs.md's note says a selective import "will import the module as well", so the selective line alone is enough. With only that line, geom.new_rect and bare square(4) both resolved.
From another module (17): function `geom.secret_tag` is private, field `geom.Rect.tag` is not public, and field `w` of struct `geom.Rect` is immutable for a pub: field. Readonly from outside, as documented.
pub mut: did not hold. docs.md says it means "public, but mutable only in parent module". In 16, module main wrote r.label = 'door' and printed label=door, exit 0. The struct-field branch of vlib/v/checker/checker.v that we read tests is_mut and nothing about the module.
Two more rules. 18 puts module shapes in geom/shapes.v; docs.md says the name "must match the name of their directory". It built and ran, so 0.5.2 did not enforce that in this layout. 19 gives modules a and b each other's import. The builder stopped with import cycle detected between the following modules: and three chains, starting main -> a -> b -> a.
§VII. Lab
Safety callout: export V_C_ERROR_BUG_REPORT_DISABLED=1 before any v command. V 0.5.2 uploads a report to bugs.vlang.io on every C compile error by default. Each report carries up to 40 V source lines on each side of the failing line, the C context around the error, and the build options. v help build-c documents the switch; docs.md does not. Set the variable to 1 in the lab script's first lines, and every V lab script does the same from here on.
- Predict the exit codes of
02,07,10b,13, and16, then run them by absolute path withV_C_ERROR_BUG_REPORT_DISABLED=1exported. - Emit C for
20_c_shapes.vand findstruct main__Shape,struct main__Namer, and the methods table. - Move
square()into a newgeom/extra/folder, import it asgeom.extra, and call it with a one-segment prefix.
Evidence: lab/lab.sh, lab/lab.log, and the .v files and project folders beside them.
§VIII. Contributor note
Four gaps a learner could draft into an issue or docs change. Each comes with a two-file reproduction already in lab/.
- Interface
mut:section. docs.md says a value receiver does not satisfy amut:method; 0.5.2 accepted it (10,10b). The example'sfn2line is commented out, sov check-mdnever tests it. Afailcompileblock would. pub mut:outside the parent module. Documented as parent-only;16mutated it frommain.- Smart cast without
mut. Documented as keeping the original type;15used the variant method. - Embedded struct passed as its embed. The checker accepts it and the C compiler rejects it (
07). That is a checker bug, not a docs gap. Search open issues before filing.
Unrelated to the language, docs.md never mentions the automatic C-error upload. A one-line pointer to V_C_ERROR_BUG_REPORT_DISABLED under Debugging is a small docs change. Duha #8 owns the draft-PR shape. Nothing goes upstream from a lesson.
§IX. Common mistakes
- Marking the binding
mutand expecting a default-section field to become writable. - Writing
import mandimport m { f }together. - Using
aswhereif x is Tormatchwould do. - Adding
else {}to a sum-typematchby habit. It turns off the exhaustiveness error that13shows. - Trusting the access table and the interface
mut:section as written. Test them on the pinned compiler. - Running V labs without
V_C_ERROR_BUG_REPORT_DISABLED=1.
§X. V next to Go and Rust, session 3
Next to Go, interfaces are implicit in both, and V's can also require fields. V adds closed sum types with exhaustive match, which Go lacks. Next to Rust, type Shape = A | B | C with match maps onto enum with match, and both refuse a missing arm. Rust's trait impls are explicit and checked by rustc. V's checker let three documented rules through today and left one to the C compiler. Verdict: the shapes are familiar and pleasant, and the docs promise more checking than 0.5.2 does. Learn it, adopt nothing. Asr #4 takes memory and concurrency tomorrow.
Related:
Fence the fields. Let the method set decide. Exhaust the sum. Close the editor.