#set document(title: "5.8 Reflection", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 5.8#h(0.6em)Reflection Reflection helps you write code that inspects its own structure at compile time and reports information about types. This makes it possible to build features like structural validation, automatic comparisons, serialization, safer assertions, and richer error messages without hardcoding details for specific type implementations. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo reflection is newly introduced and currently incomplete. Some reflection capabilities are limited, unstable, or not yet fully exposed through the language interface. This page describes the direction of the feature as well as the parts that are available today. All examples reflect the state of the language at the time this page was published. They may change as reflection support matures. ] === Why reflection? Reflection is one of Mojo's powerful compile-time features. It lets you inspect types, access fields, and generate code that adapts to a struct's shape. For example, define a struct, conform it to Equatable, and the == operator automatically works: \@fieldwise\_init struct Sensor(Equatable, Hashable, Writable): var id: Int var label: String var reading: Float64 This code uses no operator overload or boilerplate. Mojo inspects the struct at compile time, checks that each field supports equality, and generates the comparison code. That is what reflection does. Reflection has no runtime cost. The compiler does the work up front and emits code as efficient as manually written code. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ In this example, Sensor also conforms to Hashable and Writable. By conforming, instances work as Dict keys or Set elements, and print cleanly, without extra code. ] === Inspect a type Use the reflect\[T\] alias to inspect a type at compile time. Built into Mojo, it resolves to Reflected\[T\] — a handle type with static methods for querying a type. This code uses reflect\[T\] to inspect a type's structure: def show\_type\[T: AnyType\](): comptime type\_name = reflect\[T\].name() comptime field\_count = reflect\[T\].field\_count() comptime field\_names = reflect\[T\].field\_names() comptime field\_types = reflect\[T\].field\_types() print("struct", type\_name) comptime for idx in range(field\_count): comptime field\_name = field\_names\[idx\] comptime field\_type = reflect\[field\_types\[idx\]\].name() var intro = "├──" if idx \< (field\_count - 1) else "└──" print(intro, " var ", field\_name, ": ", field\_type, sep="") Create some types to test this with: \@fieldwise\_init struct MyStruct: var x: String var y: Optional\[Int\] comptime DefaultItemCount = 10 struct ParameterizedStruct\[ T: Copyable, item\_count: Int = DefaultItemCount \](Copyable): var list: List\[Self.T\] def \_\_init\_\_(out self): self.list = List\[Self.T\](capacity=Self.item\_count) def main(): show\_type\[MyStruct\](); print() show\_type\[Optional\[Float64\]\](); print() show\_type\[Dict\[Int, String\]\](); print() show\_type\[ParameterizedStruct\[String, item\_count=5\]\]() When run, this code prints each struct's name and fields with their types. The comptime for loop over fields resolves at compile time. At runtime, only the resulting print() calls execute: struct test.MyStruct ├── var x: String └── var y: std.collections.optional.Optional\[Int\] struct std.collections.optional.Optional\[SIMD\[DType.float64, 1\]\] └── var \_value: std.utils.variant.Variant\[\, {}\] struct std.collections.dict.Dict\[Int, String, std.hashlib\\ .\_ahash.AHasher\[\[0, 0, 0, 0\] : SIMD\[DType.uint64, 4\]\]\] ├── var \_table: std.collections.\_swisstable\\ .SwissTable\[Int, String, std.hashlib.\_ahash\\ .AHasher\[\[0, 0, 0, 0\] : SIMD\[DType.uint64, 4\]\]\] └── var \_order: List\[SIMD\[DType.int32, 1\]\] struct test.ParameterizedStruct\[String, 5\] └── var list: List\[String\] The output uses compiler-resolved names. Types appear fully qualified with parameters applied. If you only need the base type name, use base\_name(): print(reflect\[List\[Int\]\].base\_name()) \# List print(reflect\[Dict\[String, Int\]\].base\_name()) \# Dict #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ When you need one field, access it by name instead of iterating: comptime host\_handle = reflect\[Config\].field\_type\["host"\] var default\_host: host\_handle.T = "localhost" print(default\_host) \# localhost Name-based lookup requires a concrete type. If T is generic, use index-based iteration instead. ] === Detect field-level changes between two values Compare two values and list which fields differ. Use this for test assertions, audit logs, change tracking, or debugging. def diff\_fields\[T: AnyType\](a: T, b: T) -\> List\[String\]: comptime names = reflect\[T\].field\_names() comptime types = reflect\[T\].field\_types() var diffs = List\[String\]() comptime for idx in range(reflect\[T\].field\_count()): comptime if conforms\_to(types\[idx\], Equatable): ref a\_val = reflect\[T\].field\_ref\[idx\](a) ref b\_val = reflect\[T\].field\_ref\[idx\](b) if a\_val != b\_val: diffs.append(String(names\[idx\])) return diffs^ For example, consider a configuration type: \@fieldwise\_init struct Config(Equatable): var host: String var port: Int var verbose: Bool var timeout: Float64 diff\_fields() compares two Config values and returns the field names that differ: def main(): var old = Config("localhost", 8080, False, 30.0) var new = Config("localhost", 9090, True, 30.0) var changes = diff\_fields(old, new) for name in changes: print("changed:", name) \# changed: port \# changed: verbose === Write once, reuse everywhere with traits Reflection is powerful when used in traits with provided methods. The method runs for any conforming struct that meets the trait requirements. MakeCopyable duplicates every copyable field from one instance to another: trait MakeCopyable: def copy\_to(self, mut other: Self): comptime field\_count = reflect\[Self\].field\_count() comptime field\_types = reflect\[Self\].field\_types() comptime Usable = Copyable & ImplicitlyDeletable comptime for idx in range(field\_count): comptime field\_type = field\_types\[idx\] comptime if conforms\_to(field\_type, Usable): reflect\[Self\].field\_ref\[idx\](other) = reflect\[Self\].field\_ref\[ idx \](self).copy() Conforming structs receive copy\_to() without writing an implementation. As a trait method, copy\_to() has direct access to Self. You don't need a type parameter. \@fieldwise\_init struct MultiType(MakeCopyable, Writable): var w: String var x: Int var y: Bool var z: Float64 def write\_to\[W: Writer\](self, mut writer: W): writer.write(String(t"\[{self.w}, {self.x}, {self.y}, {self.z}\]")) def main(): var original = MultiType("Hello", 1, True, 2.5) var target = MultiType("", 0, False, 0.0) original.copy\_to(target) print(target) \# \[Hello, 1, True, 2.5\] You define the behavior once. Every conforming struct gets it as a provided method. === Layout, source locations, and type utilities These tools expose lower-level details such as layout, lifetimes, and source information. ==== Field layout and byte offsets When you need field layout for zero-copy serialization, C interop, or alignment, use field\_offset(): struct Packet: var flags: UInt8 var id: UInt32 var payload: UInt64 def show\_layout\[T: AnyType\](): comptime names = reflect\[T\].field\_names() comptime for i in range(reflect\[T\].field\_count()): comptime off = reflect\[T\].field\_offset\[index=i\]() print(names\[i\], "at byte", off) def main(): show\_layout\[Packet\]() \# flags at byte 0 \# id at byte 4 (alignment padding: 3 bytes) \# payload at byte 8 flags at byte 0 id at byte 4 payload at byte 8 field\_offset accepts name= or index= and accounts for alignment padding. The gap between flags (1 byte) and id (byte 4) shows the compiler inserting 3 bytes of padding so id aligns to a 4-byte boundary. ==== Types and origins Two functions provide compile-time access to type and lifetime information from expressions: #strong[type\_of(x)] returns the type of an expression for use in parameter positions: def make\_default\[T: AnyType & Defaultable\]() -\> T: return T() def main(): var x = 42 var y = make\_default\[type\_of(x)\]() print(y) \# 0 0 #strong[origin\_of(x)] captures the origin (lifetime and mutability) of a reference. In Mojo, every reference has an #emph[origin] that tracks which value it reads from and whether it can mutate that value. origin\_of(x) captures this information at compile time so you can thread it through function signatures. It appears in signatures where a returned reference must be tied to an input's lifetime: from std.os import abort def first\_ref\[ T: Copyable \](ref list: List\[T\]) -\> ref \[origin\_of(list)\] T: if not list: abort("empty list") return list\[0\] def main(): var l = \[1, 2, 3\] ref x = first\_ref(l) print(x) \# 1 x += 10 \# modifies the original list through the reference print(l) \# \[11, 2, 3\] l = \[\] first\_ref(l) \# aborts with "empty list" ault/bin/mojo+0x10002d9b8) \#7 0x000000010676cbbc \_mh\_execute\_header (/Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo+0x10414cbbc) \#8 0x000000010264d0b4 \_mh\_execute\_header (/Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo+0x10002d0b4) \#9 0x000000010264bbf8 \_mh\_execute\_header (/Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo+0x10002bbf8) \#10 0x000000010264e9e4 \_mh\_execute\_header (/Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo+0x10002e9e4) \#11 0x0000000182aebe00 /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: execution crashed To get a symbolicated stack trace, compile your program using \`mojo build\` with debug info enabled (e.g., \`-debug-level=line-tables\`) and execute it separately. The returned reference shares its origin with list, so the compiler knows it is valid as long as list is. Both are available without imports. ==== Source locations #strong[call\_location()] returns the caller's source location, not the location of the call\_location() call itself. When building assertions or validators, error messages are more useful when they point to the call site: from std.reflection import call\_location \@always\_inline def require( cond: Bool, msg: String = "requirement failed" ) raises: if not cond: raise Error(call\_location().prefix(msg)) def main() raises: var x = 5 require(x \> 10, "x must be \> 10") \# Error: At /path/to/file.mojo:10:5: x must be \> 10 The enclosing function must be \@always\_inline to capture the caller's location. Without this decorator, the location would point inside require(). call\_location() accepts an optional inline\_count parameter. The default (1) captures the immediate caller. Higher values skip additional levels of inlined calls. #strong[source\_location()] returns the location where source\_location() is called. This is less useful for debugging because it reports the location of the call, not the caller: from std.reflection import source\_location def log(msg: String): var loc = source\_location() print( "\[", loc.file\_name(), ":", loc.line(), "\] ", msg, sep="" ) def main(): log("starting up") \# \[/path/to/file.mojo:4:15\] starting up \[/var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-byybcg9e/ch05-017.mojo:4\] starting up ==== Function names Retrieve a function's source name or linker symbol at compile time. Use this for logging, tracing, or dispatch: from std.reflection import get\_function\_name, get\_linkage\_name def process\_data(): pass def main(): print(get\_function\_name\[process\_data\]()) \# process\_data print(get\_linkage\_name\[process\_data\]()) \# mangled symbol process\_data ch05-018::process\_data() - #strong[get\_function\_name\[func\]()] returns the name as written in source code. - #strong[get\_linkage\_name\[func\]()] returns the mangled symbol name. Both take the function as a parameter value. === Learn more - Visit the reflection #link("https://mojolang.org/docs/std/reflection/")[package documentation] for API details. - Learn more about traits.