Skip to content

Field Embedding with `using`

using embeds one struct inside another and promotes the embedded struct's fields into the outer struct's dot-access. Think of it as "flat read-access without copy": p.x means p.base.x.

Named embed, anonymous embed, outer-wins shadowing, transitive chains, and the no-subtyping rule.

14-using-embed.zolo
Playground
// Feature: using field embedding

// Syntax: `using base: Type` (named) or `using Type` (anonymous)

// Effect: promotes the embedded struct's fields into the outer struct's dot-access.

// FIELD-ONLY: methods of the embedded type are NOT forwarded.


// ---------- Struct definitions ----------


struct Entity {
  x: int,
  y: int,
}

struct Player {
  using base: Entity,
  hp: int,
}

struct Vec2 {
  x: int,
  y: int,
}

struct Particle {
  using Vec2,        // anonymous embed — implicit field name: vec2

  mass: int,
}

struct Base {
  name: str,
}

struct Derived {
  using base: Base,
  name: str,         // outer field shadows the promoted one

}

struct A { v: int }
struct B { using a: A }
struct C { using b: B }

// `using` also works in the compact (positional) struct form, including the

// inline method block: `self.x` here promotes through the `base` embed.

struct Sprite(using base: Entity, tag: str) {
  fn label(self) -> str { return "{self.tag}@{self.x},{self.y}" }
}

fn teleport(e: Entity) {
  print("{e.x},{e.y}")
}

fn main() {
  // ---------- named embed ----------

  let p = Player { base: Entity { x: 10, y: 20 }, hp: 100 }

  // Promoted field access — p.x desugars to p.base.x.

  print(p.x)    // 10

  print(p.y)    // 20

  print(p.hp)   // 100


  // The embed field is also reachable directly.

  print(p.base.x)  // 10


  // ---------- anonymous embed ----------

  let q = Particle { vec2: Vec2 { x: 3, y: 4 }, mass: 5 }
  print(q.x)      // 3  — promoted from q.vec2.x

  print(q.mass)   // 5


  // ---------- outer field wins ----------

  let d = Derived { base: Base { name: "base" }, name: "outer" }
  print(d.name)        // outer  (outer field wins)

  print(d.base.name)   // base


  // ---------- transitive ----------

  let c = C { b: B { a: A { v: 42 } } }
  print(c.v)   // 42  — promoted through B then C


  // ---------- compact (positional) form ----------

  let s = Sprite { base: Entity { x: 1, y: 2 }, tag: "hero" }
  print(s.x)          // 1      — promoted from s.base.x

  print(s.label())    // hero@1,2  — `self.x` promoted inside compact method


  // ---------- no subtyping ----------

  // Player cannot be passed where Entity is expected.

  // Pass p.base explicitly when Entity is required.

  teleport(p.base)   // 10,20

}

Named vs anonymous embed

// Named — you pick the field name.
struct Player {
  using base: Entity,
  hp: int,
}

// Anonymous — the field name is the type's last segment, lowercased.
struct Particle {
  using Vec2,   // implicit field name: vec2
  mass: int,
}

Construction always uses the real field name:

let p = Player { base: Entity { x: 0, y: 0 }, hp: 100 }
let q = Particle { vec2: Vec2 { x: 1, y: 2 }, mass: 3 }

What is (and isn't) promoted

  • Fields of the embedded type are promoted — p.x works.
  • Methods and trait impls of the embedded type are not forwarded. Call p.base.greet() explicitly.
  • Player is not a subtype of Entity. Pass p.base when a function expects an Entity.
  • Promotion only applies to statically typed receivers (let p: Player = ..., parameters, self, struct literals). A dynamically typed receiver is left as-is.

Priority and ambiguity

An outer field always shadows a same-named promoted field:

struct Derived {
  using base: Base,
  name: str,   // wins over Base.name
}

If two using fields promote the same name with no outer field to resolve the tie, the compiler raises TE112. Qualify the access explicitly: d.left.value or d.right.value.

Transitive embedding

Promotion chains through multiple using levels:

struct A { v: int }
struct B { using a: A }
struct C { using b: B }

let c = C { b: B { a: A { v: 42 } } }
print(c.v)   // 42

Errors

Code Meaning
TE112 Two using fields promote the same name — qualify the access.
TE113 The using field's type is not a struct (e.g. using base: int).

LSP note

Hover and go-to-definition on a promoted field (e.g. p.x) currently return no result — the LSP does not run the using desugar pass, so it does not recognize promoted field accesses. Use the qualified form p.base.x in the editor to get hover and navigation. Full promoted-field IDE support is a known follow-up.

enespt-br