#set document(title: "5.7 Materializing compile-time values at run time", 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.7#h(0.6em)Materializing compile-time values at run time Mojo's compile-time metaprogramming makes it easy to make calculations at compile time for later use. The process of making a #emph[compile-time value] available at run time is called #emph[materialization]. For types that can be trivially copied, this isn't an issue. The compiler can simply insert the value into the compiled program wherever it's needed. comptime threshold: Int = some\_calculation() \# calculate at compile time for i in range(1000): my\_function(i, threshold) \# use value at runtime However, Mojo also allows you to create instances of much more complex types at compile-time: types that dynamically allocate memory, like List and Dict. Re-using these values at run time presents some questions, like where the memory is allocated, who owns the values, and when the values are destroyed. This page describes when Mojo materializes values, and presents some techniques for avoiding unnecessary materialization of complex values. === Implicit and explicit materialization When you use a comptime value at run time, you're explicitly or implicitly copying the value into a run-time variable: comptime comptime\_value = 1000 var runtime\_value = comptime\_value This process of moving a compile-time value to a run-time variable is called #emph[materialization]. If the value is implicitly copyable, like an Int or Bool, Mojo treats it as #emph[implicitly materializable] as well. But types that #strong[aren't] implicitly copyable present other challenges. Consider the following code: def lookup\_fn(count: Int): comptime list\_of\_values = \[1, 3, 5, 7\] for i in range(count): \# Some computation, doesn't matter what it is. idx = dynamic\_function(i) \# Look up another value lookup = list\_of\_values\[idx\] \# Use the value process(lookup) This looks reasonable, but compiling it produces an error on this line: lookup = list\_of\_values\[idx\] cannot materialize comptime value of type 'List\[Int\]' to runtime because it is not 'ImplicitlyCopyable' Just like Mojo forces you to explicitly copy a value that's expensive to copy, it forces you to explicitly materialize values that are expensive to materialize, by calling the #link("https://mojolang.org/docs/std/builtin/value/materialize/")[materialize()] function. Here's the code above with explicit materialization added: def lookup\_fn(count: Int): comptime list\_of\_values = \[1, 3, 5, 7\] for i in range(count): idx = dynamic\_function(i) \# This is the problem var tmp: List\[Int\] = materialize\[list\_of\_values\]() lookup = tmp\[idx\] \# tmp is destroyed here process(lookup) This code materializes the list of values #emph[inside] of the loop, which includes dynamically allocating heap memory and storing the four elements into that memory. Because the last use of tmp is on the next line, the memory then gets deallocated before the loop iterates. This creates and destroys the list on every iteration of the loop, which is clearly wasteful. A more efficient version would materialize the list #emph[outside] of the loop: def lookup\_fn(count: Int): comptime list\_of\_values = \[1, 3, 5, 7\] var list = materialize\[list\_of\_values\]() for i in range(count): idx = dynamic\_function(i) lookup = list\[idx\] process(lookup) \# materialized list is destroyed here This is why Mojo requires you to explicitly materialize non-trivial values; it puts you in control of when your program allocates resources. === Global lookup tables Mojo doesn't currently have a general-purpose mechanism for creating global static data. This is a problem for some performance-sensitive code where you want to use a static lookup table. Even if you declare the table as a comptime value, you need to materialize it each time you want to use the data. The #link("https://mojolang.org/docs/std/builtin/globals/global_constant/")[global\_constant()] function provides a solution for storing a compile-time value into static global storage, so you can access it without repeatedly materializing the value. However, this currently only works for self-contained values which don't include pointers to other locations in memory. That rules out using collection types like List and Dict. The easiest way to use global\_constant() is with #link("https://mojolang.org/docs/std/collections/inline_array/InlineArray/")[InlineArray], which allocates a statically sized array of elements on the stack. The following code uses global\_constant() to create a static lookup table. from std.builtin.globals import global\_constant def use\_lookup(idx: Int) -\> Int64: comptime numbers: InlineArray\[Int64, 10\] = \[ 1, 3, 14, 34, 63, 101, 148, 204, 269, 343 \] ref lookup\_table = global\_constant\[numbers\]() if idx \>= len(lookup\_table): return 0 return lookup\_table\[idx\] def main(): print(use\_lookup(3)) 34 At compile time, Mojo allocates the numbers array, and then the global\_constant() function copies it into static constant memory, where the code can reference it without requiring any dynamic logic to create or populate the array. At run time, the lookup\_table identifier receives an immutable reference to this memory. Note the use of ref lookup\_table to bind the reference returned by global\_constant(). Using var lookup\_table would cause a compiler error, because it would trigger a copy, and InlineArray doesn't support implicit copying. === Using the comptime keyword Another approach that you can use to avoid materializing a complex value is to use the comptime keyword to control when Mojo evaluates an expression. Assigning an expression to a comptime value causes Mojo to evaluate the expression at compile time. For example, if you want to force a function to run at compile time: comptime tmp = calculate\_something() \# executed at compile time var y = x \* tmp \# executed at run time If you're only creating a comptime value for a single use, you can use a comptime sub-expression instead: var y = x \* comptime (calculate\_something()) This works exactly like the previous example, without creating a named temporary value. The comptime keyword here tells Mojo to evaluate the expression inside the parentheses (calculate\_something()) at compile time. For example, you can use a comptime sub-expression when working with the Layout type, which determines how you store and retrieve data in a LayoutTensor. Materializing a Layout requires dynamic allocation, which isn't supported on GPUs. So calling this code on a GPU produces an error: comptime layout = Layout.row\_major(16, 8) var x = layout.size() // WARP\_SIZE \# Can't implicitly materialize layout A comptime sub-expression fixes this issue: comptime layout = Layout.row\_major(16, 8) var x = comptime (layout.size()) // WARP\_SIZE Now, the expression layout.size() gets evaluated at compile time, so there's no need to materialize the layout. You could also achieve the same effect using a named comptime value. comptime layout = Layout.row\_major(16, 8) comptime layout\_size = layout.size() var x = layout\_size // WARP\_SIZE The comptime sub-expression is just a more compact way to express the same thing. === Materializing literals Literal values, like string literals and numeric literals are also materialized to their run-time equivalents, but this is mostly handled automatically by the compiler: comptime str\_literal = "Hello" \# at compile time, a StringLiteral var str = str\_literal \# at run time, a String. var static\_str: StaticString = str\_literal \# or a StaticString Both String and StaticString can be implicitly created from a StringLiteral, but without a type annotation, Mojo defaults to materializing StringLiteral as a String.