Skip to content

Instantly share code, notes, and snippets.

@planetis-m
Last active July 2, 2026 15:58
Show Gist options
  • Select an option

  • Save planetis-m/449bc45ea8befa1f64c668ee358aacc5 to your computer and use it in GitHub Desktop.

Select an option

Save planetis-m/449bc45ea8befa1f64c668ee358aacc5 to your computer and use it in GitHub Desktop.
## Extended validation: diamond inheritance + heterogeneous polymorphism
# ============================================================
# Shared base (top of diamond)
# ============================================================
type
Positioned = ref object of RootObj
x, y: int
proc initPositioned(self: Positioned, x, y: int) =
self.x = x
self.y = y
proc posStr(self: Positioned): string =
"(" & $self.x & "," & $self.y & ")"
# ============================================================
# Two branches of the diamond
# ============================================================
type
Movable = ref object of RootObj
base_Positioned: Positioned # shares the SAME Positioned instance
proc initMovable(self: Movable, p: Positioned) =
self.base_Positioned = p
proc moveBy(self: Movable, dx, dy: int) =
self.base_Positioned.x += dx
self.base_Positioned.y += dy
type
Colored = ref object of RootObj
base_Positioned: Positioned # shares the SAME Positioned instance
color: string
proc initColored(self: Colored, p: Positioned, color: string) =
self.base_Positioned = p
self.color = color
proc recolor(self: Colored, c: string) =
self.color = c
# ============================================================
# Bottom of diamond: inherits both Movable and Colored
# Both share the same Positioned instance -> no ambiguity
# ============================================================
type
GameEntity = ref object of RootObj
base_Movable: Movable
base_Colored: Colored
shared_pos: Positioned # single instance, passed to both branches
label: string
# Forwarding thunks
proc moveBy(self: GameEntity, dx, dy: int) =
moveBy(self.base_Movable, dx, dy)
proc recolor(self: GameEntity, c: string) =
recolor(self.base_Colored, c)
proc posStr(self: GameEntity): string =
posStr(self.shared_pos)
proc colorStr(self: GameEntity): string =
self.base_Colored.color
proc newGameEntity(x, y: int, color, label: string): GameEntity =
new result
result.shared_pos = Positioned()
initPositioned(result.shared_pos, x, y)
result.base_Movable = Movable()
result.base_Colored = Colored()
initMovable(result.base_Movable, result.shared_pos)
initColored(result.base_Colored, result.shared_pos, color)
result.label = label
# ============================================================
# Polymorphism: heterogeneous collection via role interface
# ============================================================
type
MovableIVT = object
moveBy: proc(self: RootRef, dx, dy: int) {.nimcall.}
posStr: proc(self: RootRef): string {.nimcall.}
MovableI = object
obj: RootRef
vtable: ptr MovableIVT
# Concrete type #2 (different from GameEntity)
type
Projectile = ref object of RootObj
px, py: int
proc moveBy(self: Projectile, dx, dy: int) =
self.px += dx
self.py += dy
proc posStr(self: Projectile): string =
"(" & $self.px & "," & $self.py & ")"
proc newProjectile(x, y: int): Projectile =
new result
result.px = x
result.py = y
# Per-concrete-type vtables
proc gameEntityMovableVT(): ptr MovableIVT =
var v {.global.} = MovableIVT(
moveBy: proc(self: RootRef, dx, dy: int) {.nimcall.} =
moveBy(cast[GameEntity](self), dx, dy),
posStr: proc(self: RootRef): string {.nimcall.} =
posStr(cast[GameEntity](self))
)
addr v
proc projectileMovableVT(): ptr MovableIVT =
var v {.global.} = MovableIVT(
moveBy: proc(self: RootRef, dx, dy: int) {.nimcall.} =
moveBy(cast[Projectile](self), dx, dy),
posStr: proc(self: RootRef): string {.nimcall.} =
posStr(cast[Projectile](self))
)
addr v
proc toMovable(e: GameEntity): MovableI =
MovableI(obj: cast[RootRef](e), vtable: gameEntityMovableVT())
proc toMovable(p: Projectile): MovableI =
MovableI(obj: cast[RootRef](p), vtable: projectileMovableVT())
# Interface forwarders
proc moveBy(self: MovableI, dx, dy: int) =
self.vtable.moveBy(self.obj, dx, dy)
proc posStr(self: MovableI): string =
self.vtable.posStr(self.obj)
# ============================================================
# Test
# ============================================================
when isMainModule:
echo "=== Diamond: GameEntity inherits Movable + Colored, both share Positioned ==="
var e = newGameEntity(0, 0, "red", "player")
# Move via Movable branch -> updates shared Positioned
moveBy(e, 5, 10)
echo " after move: ", posStr(e)
# Recolor via Colored branch
recolor(e, "blue")
echo " color: ", colorStr(e)
# Diamond consistency: both branches see the same position
echo " Movable sees: ", posStr(e.base_Movable.base_Positioned)
echo " Colored sees: ", posStr(e.base_Colored.base_Positioned)
echo " diamond consistent: ",
e.base_Movable.base_Positioned == e.base_Colored.base_Positioned
echo ""
echo "=== Heterogeneous polymorphism via MovableI ==="
var entities: seq[MovableI] = @[]
entities.add toMovable(newGameEntity(100, 200, "green", "npc"))
entities.add toMovable(newProjectile(0, 0))
entities.add toMovable(newGameEntity(50, 50, "yellow", "chest"))
for i, ent in entities:
moveBy(ent, 1, 1)
echo " entity ", i, " now at ", posStr(ent)
## Hypothetical emitted code for a MI macro.
## Three layers: composition (state MI) + forwarding (code MI) + role interfaces (polymorphism).
## This is NOT a macro — it's exactly what the macro would emit, validated to compile and run.
# ============================================================
# Layer 1: Base types (standalone "classes")
# ============================================================
type
Point = object
x, y: int
Drawable = ref object of RootObj
pos: Point
Named = ref object of RootObj
name: string
# Base methods — real implementations
proc initDrawable(self: Drawable) =
self.pos = Point(x: 0, y: 0)
proc move(self: Drawable, dx, dy: int) =
self.pos.x += dx
self.pos.y += dy
proc initNamed(self: Named) =
self.name = ""
proc setName(self: Named, s: string) =
self.name = s
proc getName(self: Named): string =
self.name
# ============================================================
# Layer 1: Concrete type — multiple inheritance via composition
# ============================================================
type
Sprite = ref object of RootObj
base_Drawable: Drawable # embedded base #1
base_Named: Named # embedded base #2
extra: int # own state
# ============================================================
# Layer 2: Code MI — forwarding thunks + override
# ============================================================
# Forwarded (not overridden) -> delegate to embedded base
proc move(self: Sprite, dx, dy: int) =
move(self.base_Drawable, dx, dy)
proc setName(self: Sprite, s: string) =
setName(self.base_Named, s)
proc getName(self: Sprite): string =
getName(self.base_Named)
# Drawable has a draw method
proc draw(self: Drawable, ctx: int) =
echo " Drawable.draw at (", self.pos.x, ", ", self.pos.y, ")"
# Override in Sprite — can call base explicitly
proc draw(self: Sprite, ctx: int) =
echo " Sprite.draw at (", self.base_Drawable.pos.x, ", ",
self.base_Drawable.pos.y, ") extra=", self.extra
draw(self.base_Drawable, ctx) # explicit base call
# Constructor — initializes all bases
proc newSprite(): Sprite =
new result
result.base_Drawable = Drawable()
result.base_Named = Named()
initDrawable(result.base_Drawable)
initNamed(result.base_Named)
result.extra = 42
# ============================================================
# Layer 3: Role interfaces — per-role vtable, whole-object handle
# ============================================================
# --- Drawable role ---
type
DrawableIVT = object
draw: proc(self: RootRef, ctx: int) {.nimcall.}
move: proc(self: RootRef, dx, dy: int) {.nimcall.}
DrawableI = object
obj: RootRef
vtable: ptr DrawableIVT
proc spriteDrawableVT(): ptr DrawableIVT =
var v {.global.} = DrawableIVT(
draw: proc(self: RootRef, ctx: int) {.nimcall.} =
draw(cast[Sprite](self), ctx),
move: proc(self: RootRef, dx, dy: int) {.nimcall.} =
move(cast[Sprite](self), dx, dy)
)
addr v
proc toDrawable(s: Sprite): DrawableI =
DrawableI(obj: cast[RootRef](s), vtable: spriteDrawableVT())
# Interface forwarders
proc draw(self: DrawableI, ctx: int) =
self.vtable.draw(self.obj, ctx)
proc move(self: DrawableI, dx, dy: int) =
self.vtable.move(self.obj, dx, dy)
# --- Named role ---
type
NamedIVT = object
setName: proc(self: RootRef, s: string) {.nimcall.}
getName: proc(self: RootRef): string {.nimcall.}
NamedI = object
obj: RootRef
vtable: ptr NamedIVT
proc spriteNamedVT(): ptr NamedIVT =
var v {.global.} = NamedIVT(
setName: proc(self: RootRef, s: string) {.nimcall.} =
setName(cast[Sprite](self), s),
getName: proc(self: RootRef): string {.nimcall.} =
getName(cast[Sprite](self))
)
addr v
proc toNamed(s: Sprite): NamedI =
NamedI(obj: cast[RootRef](s), vtable: spriteNamedVT())
# Interface forwarders
proc setName(self: NamedI, s: string) =
self.vtable.setName(self.obj, s)
proc getName(self: NamedI): string =
self.vtable.getName(self.obj)
# ============================================================
# Cross-interface navigation (identity preserved)
# ============================================================
proc sameObject(a: DrawableI, b: NamedI): bool =
a.obj == b.obj
# ============================================================
# Test
# ============================================================
when isMainModule:
echo "=== Create Sprite (inherits Drawable + Named) ==="
var s = newSprite()
echo "=== Obtain two role views ==="
let d = s.toDrawable()
let n = s.toNamed()
echo "=== Use Drawable interface ==="
d.move(10, 20)
d.draw(0) # dispatches to Sprite.draw (override)
echo "=== Use Named interface ==="
n.setName("hero")
echo " name via NamedI: ", n.getName()
echo "=== Identity check ==="
echo " d.obj == n.obj: ", sameObject(d, n)
echo "=== Verify state through concrete type ==="
echo " pos: (", s.base_Drawable.pos.x, ", ", s.base_Drawable.pos.y, ")"
echo " name: ", s.base_Named.name
echo " extra: ", s.extra

My opinion: the best MI is two layers that don't fight each other

  1. Implementation MI (state + code) via composition + this-adjustment forwarding — embed each base as a subobject, generate a forwarding proc per inherited method.
  2. Polymorphism MI (heterogeneous dispatch) via per-concrete-type role interfaces — a fat pointer holding the whole object plus a vtable whose thunks cast back to the concrete type.

Crucially the interface value holds the whole object, so two different role-views of the same instance share one pointer → identity is preserved and cross-interface as is trivial. This is the fix for macro #1's fatal flaw.

Conceptual input

class Drawable:
  pos: Point
  method draw(self, ctx)
  method move(self, dx, dy)

class Named:
  name: string
  method setName(self, s)

class Sprite: Drawable, Named:        # <- multiple inheritance
  extra: int
  method draw(self, ctx) = ...        # override

Layer 1 — state MI: bases become embedded subobjects

type
  Drawable = ref object of RootRef
    pos: Point

  Named = ref object of RootRef
    name: string

  Sprite = ref object of RootRef      # flat — NOT "of Drawable"
    base_Drawable: Drawable           # one subobject per base
    base_Named: Named
    extra: int

Layer 2 — code MI: forwarding thunks + override + explicit base call

# inherited, not overridden -> delegate to the embedded base's impl
proc move(self: Sprite, dx, dy: int) =
  move(self.base_Drawable, dx, dy)

proc setName(self: Sprite, s: string) =
  setName(self.base_Named, s)

# overridden -> Sprite's own body; can still reach the base explicitly
proc draw(self: Sprite, ctx: ptr CTXRender) =
  echo "sprite at ", self.base_Drawable.pos
  draw(self.base_Drawable, ctx)       # explicit base call (no 'super' needed)

Construction initializes every base subobject:

proc newSprite(): Sprite =
  new result
  result.base_Drawable = Drawable(pos: Point(0,0))
  result.base_Named    = Named(name: "")
  result.extra = 0

This already delivers full C++-style implementation MI: many bases, shared state, code reuse, overrides, explicit base dispatch. Diamond is unambiguous — you always name the path (self.base_Drawable... vs self.base_Named...).

Layer 3 — polymorphism: a role interface per face, whole-object handle

type
  DrawableIVT = object                # method set of the Drawable role
    draw: proc(self: RootRef, ctx: ptr CTXRender)
    move: proc(self: RootRef, dx, dy: int)

  DrawableI = object                  # fat pointer: whole object + per-type vtable
    obj: RootRef
    vtable: ptr DrawableIVT

# one vtable per concrete type that implements the role;
# thunks cast back to the concrete type and call the (possibly overridden) method
proc spriteDrawableVT(): ptr DrawableIVT =
  var v {.global.} = DrawableIVT(
    draw: proc(self: RootRef, ctx: ptr CTXRender) = draw(cast[Sprite](self), ctx),
    move: proc(self: RootRef, dx, dy: int)        = move(cast[Sprite](self), dx, dy))
  addr v

proc toDrawable(s: Sprite): DrawableI =
  DrawableI(obj: cast[RootRef](s), vtable: spriteDrawableVT())

The Named role is emitted symmetrically (NamedI, spriteNamedVT, toNamed). Now the payoff:

var s = newSprite()
let d = s.toDrawable()
let n = s.toNamed()
d.draw(ctx)                           # dispatches to Sprite.draw (the override)
n.setName("hero")
echo d.obj == n.obj                   # true — same identity, recoverable

Because both views hold the same RootRef, navigation between roles is a pointer comparison. Add a tiny type-id and you get as:

type Any = object
  obj: RootRef
  typeID: TypeID                      # which concrete type this is
  vtable: ptr RoleTable               # table of role-name -> vtable for this type

proc as(a: Any, Role: typedesc): Option[Role] =
  if a.typeID in Role.implementors:   # consult a compile-time/RTTI registry
    some(castRole(a, Role)) else: none

Why this is the best shape

  • No layout coupling between roles (each face is independent) → no diamond problem in the polymorphic layer.
  • Real state + code reuse in the composition layer → genuine implementation MI, not just contracts.
  • Identity preserved because the interface handle points at the whole object, not an erased slice — the one thing macro #1 got wrong.
  • Virtual dispatch follows overrides because the per-type vtable thunk casts to the concrete type and calls the resolved method, so deriving further (Logo of Sprite) just needs toDrawable(logo) to pick up LogoVT.
  • Every emitted construct is ordinary Nim (refs, fields, procs, a {.global.} vtable, a cast) — no {.emit}, no C, no pointer arithmetic, works on all backends.

The two earlier macros each implemented half of this: macro #1 got layer 3 but with a broken identity; macro #2 got a single-inheritance vtable. Combining layer 1+2 (composition + forwarding) for implementation with layer 3 (whole-object role interfaces) for polymorphism is, in my view, the cleanest MI you can emit in Nim.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment