using on an embedded struct field promotes the inner struct's field names one level up, into the outer struct's scope. Write e.x and the compiler reaches through to e.xform.x for you — same storage, two names. Pick a field below and watch which path it travels.
Level 1 was the struct form. The same keyword does the same job — pull inner names up one scope — in two more places. Then comes the one sharp edge: when two promotions reach for the same name.
One keyword, one rule (the inner names become available one level up), three spots it shows up:
using xform: Transform inside Entity (Level 1). e.x reaches through to e.xform.x.print_entity :: proc(using e: Entity) lets the body write x, y, z, hp with no e. prefix — the parameter's fields read like locals. Calling it on the Level-1 entity prints entity at (1, 2, 3) hp=100.using Color brings Red, Green, Blue into scope as bare identifiers, so c: Color = Red (no Color., no .Red). This form is off by default — see the gotcha below.The gotcha — two promotions, one name. Promote a field name into the outer scope and it now lives there as a bare name. So if a second using struct promotes a field with the same name, the bare name is ambiguous and the build stops. Add Health :: struct { x: int, hp: int } and embed it alongside the Transform, both with using — both want to own the bare name x:
Entity :: struct { using xform: Transform, // promotes x, y, z using health: Health, // also promotes x ← collision } main.odin(6:2) Error: 'x' is already declared in 'struct {using xform: Transform, using health: Health}', through 'using' from 'Health'
The fix: drop one of the usings and reach that struct through its named field (e.health.x). The promotion is an opt-in convenience for one obvious base; the moment two of them fight over a name, the convenience is gone and the explicit path is clearer anyway.
A different flattening: a procedure group binds several type-specialized procs under one name. area :: proc{area_circle, area_square, area_triangle} — and the compiler picks the matching member by the argument's type, at compile time. Pick a call and watch which concrete proc fires.
You declare each specialization as a normal proc with its own name, then list them inside proc{...}. The group has no parameter list of its own — its members carry the signatures. At each call site the compiler matches the argument type against the members and emits a direct call to the one that fits:
| call site | compiler picks | result (f32) |
|---|
It's resolved, then it's gone. Once the compiler picks area_circle for area(c), the call site is a plain direct call to area_circle — indistinguishable from writing that name by hand. The group is a compile-time alias: there's no stored proc value, no table lookup, no runtime dispatch. The original member names also still work directly, and area_circle(c) produces the identical result as area(c) — same call, two spellings.
Where you've already met this. Every standard-library name that "looks like one proc but takes many types" is a group. fmt.println routes to per-type printers; append routes between append_elem, append_elems, append_string, and more. The convention is fixed: the user-facing name is the group, and the implementations are named <group>_<thing> and listed inside the braces. When a core: proc seems to take "anything", search for name :: proc{ and you'll find the exact dispatch list.
The payoff emerges from the group being a closed, listed set. There's no best-match scoring and no silent conversion to wedge an argument in: an argument either matches a member's type or the call doesn't exist. When none match, the compiler hands you the whole considered list and says why.
Call area with a type no member accepts — say a string — and the build stops. The error isn't a vague "no such function": it enumerates every candidate it weighed and the argument type it couldn't place:
area :: proc{area_circle, area_square} // ... _ = area("hello") // a string — no member takes one main.odin(19:6) Error: No procedures or ambiguous call for procedure group 'area' that match with the given arguments Given argument types: • untyped string Did you mean one of the following overloads? main.area_circle :: proc(c: Circle) -> f32 main.area_square :: proc(s: Square) -> f32
The emergent payoff: the candidate set is closed and visible from one declaration. Nobody can extend area from another file by happening to declare a proc with a matching name — the only members are the ones inside the braces. And because each candidate has a unique name and an exact-match-or-nothing rule (no implicit conversions to argue over), the resolution has no surprises: the compiler's job is "find the one whose parameter type fits", not "score N near-misses and hope you guessed the same winner". Reading a call, you can find the dispatch list; reading the error, you see exactly what was tried.
That's the arc: L1 using promotes inner field names up a scope — same bytes, shorter name, zero cost → L2 the same keyword works on a parameter and (opt-in) on an enum, and its one sharp edge is two promotions colliding on a name → L3 a procedure group bundles type-specialized procs under one name and the compiler routes by argument type, then compiles to a direct call → L4 that group is a closed, listed set, so dispatch has no surprises and a bad argument gets the whole considered list back.