From 00c6c864db862dc28aa77871d8fbd3f153905414 Mon Sep 17 00:00:00 2001 From: Chloe Dancey <22119302+novo52@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:40:32 -0600 Subject: [PATCH 1/5] Added advice for students --- gazprea/impl/backend.rst | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/gazprea/impl/backend.rst b/gazprea/impl/backend.rst index cd265e9..0ba1356 100644 --- a/gazprea/impl/backend.rst +++ b/gazprea/impl/backend.rst @@ -6,6 +6,163 @@ Backend You don’t need to implement an interpreter for Gazprea. You only need to implement a *MLIR* code generator that outputs *LLVM IR*. +.. _ssec:representing_values: + +Representing Values +------------------- + +When you emit MLIR you must decide *how a value lives*: as an SSA value that is +defined once, or as a slot in memory that you load from and store to. Pick one deliberately. + +.. note:: + + **In practice.** The memory model (``alloca`` / ``load`` / ``store`` + + ``mem2reg``) is standard for imperative, systems-style languages with an + explicit, language-defined memory layout, like C. Value-semantic ``tensor`` + representations are favored at the high level by array- and math-focused + compilers like Fortran and Tensorflow. + +**The memory model.** Give every value a slot: reads are loads, writes are stores. +The mutable state lives in memory and the optimizer's ``mem2reg`` promotion turns the slots back into SSA +registers for you. A scalar declaration is an ``alloca``, a read a ``load``, an +assignment a ``store``: + +:: + + // var integer n = 42; then n = n + 1; + %n = memref.alloca() : memref + memref.store %c42, %n[] : memref + %0 = memref.load %n[] : memref + %1 = arith.addi %0, %c1 : i32 + memref.store %1, %n[] : memref + +It pairs naturally with unstructured ``cf``: because the state is in memory, +``break`` / ``continue`` / ``return`` are ordinary branches — to the loop's exit +block, its latch, and the function exit — with nothing threaded through them and +no analysis of which variables cross a construct. + +**The value (SSA) model.** Map each variable name to its *current* SSA value: a +read is a lookup, an assignment produces a new value and rebinds the name. There +are no slots and no loads or stores; bufferization introduces memory later, +downstream of your emitter. The one hard case is a *merge*: after an ``if``, a +variable set on only one branch still needs a single value afterward. The +structured ``scf`` ops hand you this by *yielding* merged values as region results +(``scf.if`` results, ``scf.for`` / ``scf.while`` ``iter_args``) rather than making +you place a merge: + +:: + + // if (c) { x = 1; } else { x = 2; } -- x lives after the if + %x = scf.if %c -> (i32) { + scf.yield %c1 : i32 + } else { + scf.yield %c2 : i32 + } + +``scf`` requires you to declare the set of values a region carries (those it +modifies that are still needed afterward). ``scf`` also has no early exit — a region runs +to its ``scf.yield`` — so Gazprea's ``break``, ``continue``, and ``return`` need a +way to be represented *inside* a structured region; work out how before you commit +to this model. + +.. Warning:: + Avoid emitting unstructured ``cf`` blocks and + placing the phi / block-argument merges yourself. + +.. Warning:: + The SSA model is the more difficult of the two to implement, but can allow for more + optimization to occur. + +.. _ssec:representing_arrays: + +Representing Arrays and Aggregate Types +--------------------------------------- + +Arrays, matrices, and vectors can live in memory or as values, the same choice as +for scalars, but their interaction with tuples/structs is a complication. + +Vector representation +~~~~~~~~~~~~~~~~~~~~~ + +A ``{ptr, len, capacity}`` struct or similar is the standard way to implement growable memory. + +.. Note:: + For performance marks, consider how your vector grows. + +Array representation +~~~~~~~~~~~~~~~~~~~~ + +Three options, differing in ease of implementation and +optimization potential. + +- **A** ``memref`` **(memory model).** Allows optimization via ``linalg.generic``, but + gives up + fusion: a chain like ``a * b + a`` is two loops with an intermediate buffer. + +- **A** ``tensor`` **(value model).** + Element assignment ``v[i] = e`` is a ``tensor.insert`` that *yields a new value* + you rebind, threaded exactly like a scalar. Fusion is available — a pure + elementwise chain of ``tensor`` values fuses into one loop — but it must happen + while the arrays are still ``tensor`` (once bufferized they are memory). + +- **A** ``{ptr, len, cap}`` **struct**: The simplest option, uniform with vectors. + You give + up the standard-dialect machinery — no ``linalg``, no ``memref`` / ``tensor`` + passes. + +Allocation, ownership, and freeing for all three are covered in +:ref:`ssec:backend_memory`. + +Array fields inside structs and tuples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A struct or tuple may have an +array-typed field — ``struct s1 (integer i, real r, integer[10] iv)``, +``tuple(integer, real, integer[10])``. A standalone array can use any of the three above +representations; an array within a struct must exist as a valid representation +inside the aggregate or be lifted out of it. + +.. Warning:: + ``!llvm.struct`` and ``!llvm.struct`` will not lower, only an + LLVM-typed field may nest inside an ``llvm.struct``. A ``{ptr, len, cap}`` + struct *is* an LLVM type and nests freely. + +**Option 1 — one representation everywhere.** Use a ``{ptr, len, cap}`` or ``{ptr, len}`` struct for +**every** sequence: arrays, vectors, strings, matrices. A struct or tuple is then +an ``!llvm.struct`` of its fields, and an array field is just a ``{ptr, len}`` +member — nesting is free and has no special case. + +**Option 2 — split by mutability.** Represent arrays and matrices as +``memref`` or ``tensor``. Because a ``memref`` / ``tensor`` cannot nest in an ``!llvm.struct``, an array that +appears as an aggregate field is handled by **SoA decomposition**: the aggregate is +split into its leaves, and each array field becomes its own top-level ``memref`` / +``tensor`` value threaded alongside the others rather than a member of one struct +object. A ``{i, r, iv}`` struct is carried as the parallel leaves ``i32``, +``f64``, ``memref<10xi32>``; there is no single aggregate value. Vectors, being +``{ptr, len, cap}``, still nest normally. The cost is two sequence representations +plus the decomposition machinery — a 1:N type conversion that splits aggregates +into leaves and carries them across call, return, and loop boundaries. The payoff +is greater array and matrix optimization. + +.. Warning:: + This is lots of work to implement. + +.. Note:: + Under Struct of Arrays (SoA) decomposition, the following transformation occurs: + :: + tuple(integer, real[10], integer[2]) data; + data.2[5] = 4; + + becomes + :: + integer data_1; + real[10] data_2; + integer[2] data_3; + data_2[5] = 4; + + In the resulting program, there are no arrays in aggregates. + + .. _ssec:backend_memory: Memory Management From c8eae807c841fadfe83fbe588d394c2dc60bee25 Mon Sep 17 00:00:00 2001 From: Chloe Dancey <22119302+novo52@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:08:31 -0600 Subject: [PATCH 2/5] Rework backend value-representation section; use LLVM dialect with opaque pointers --- gazprea/impl/backend.rst | 187 +++++++++++---------------------------- 1 file changed, 52 insertions(+), 135 deletions(-) diff --git a/gazprea/impl/backend.rst b/gazprea/impl/backend.rst index 0ba1356..be45471 100644 --- a/gazprea/impl/backend.rst +++ b/gazprea/impl/backend.rst @@ -11,8 +11,13 @@ implement a *MLIR* code generator that outputs *LLVM IR*. Representing Values ------------------- -When you emit MLIR you must decide *how a value lives*: as an SSA value that is -defined once, or as a slot in memory that you load from and store to. Pick one deliberately. +When you emit MLIR you must decide *how a value lives*: as a slot in memory that +you load from and store to, or as an SSA value that is defined once. **Use the +memory model.** It keeps code generation local and mechanical. The choice is the +same for scalars and for arrays, matrices, vectors, and aggregates, so this section +treats them together. This section also describes the value (SSA) model, because +understanding it explains what the optimizer does for you, but you are not expected +to implement it. .. note:: @@ -22,146 +27,58 @@ defined once, or as a slot in memory that you load from and store to. Pick one d representations are favored at the high level by array- and math-focused compilers like Fortran and Tensorflow. -**The memory model.** Give every value a slot: reads are loads, writes are stores. -The mutable state lives in memory and the optimizer's ``mem2reg`` promotion turns the slots back into SSA -registers for you. A scalar declaration is an ``alloca``, a read a ``load``, an -assignment a ``store``: +The memory model +~~~~~~~~~~~~~~~~ +**Use this.** + +*Scalars.* Give every value a slot: reads are loads, writes are stores. The mutable +state lives in memory and the optimizer's ``mem2reg`` promotion turns the slots back +into SSA registers for you. A scalar declaration is an ``alloca``, a read a +``load``, an assignment a ``store``: :: // var integer n = 42; then n = n + 1; - %n = memref.alloca() : memref - memref.store %c42, %n[] : memref - %0 = memref.load %n[] : memref - %1 = arith.addi %0, %c1 : i32 - memref.store %1, %n[] : memref + %size = llvm.mlir.constant(1 : i64) : i64 + %n = llvm.alloca %size x i32 : (i64) -> !llvm.ptr + %c42 = llvm.mlir.constant(42 : i32) : i32 + llvm.store %c42, %n : i32, !llvm.ptr + %0 = llvm.load %n : !llvm.ptr -> i32 + %c1 = llvm.mlir.constant(1 : i32) : i32 + %1 = llvm.add %0, %c1 : i32 + llvm.store %1, %n : i32, !llvm.ptr It pairs naturally with unstructured ``cf``: because the state is in memory, -``break`` / ``continue`` / ``return`` are ordinary branches — to the loop's exit -block, its latch, and the function exit — with nothing threaded through them and -no analysis of which variables cross a construct. - -**The value (SSA) model.** Map each variable name to its *current* SSA value: a -read is a lookup, an assignment produces a new value and rebinds the name. There -are no slots and no loads or stores; bufferization introduces memory later, -downstream of your emitter. The one hard case is a *merge*: after an ``if``, a -variable set on only one branch still needs a single value afterward. The -structured ``scf`` ops hand you this by *yielding* merged values as region results -(``scf.if`` results, ``scf.for`` / ``scf.while`` ``iter_args``) rather than making -you place a merge: - -:: - - // if (c) { x = 1; } else { x = 2; } -- x lives after the if - %x = scf.if %c -> (i32) { - scf.yield %c1 : i32 - } else { - scf.yield %c2 : i32 - } - -``scf`` requires you to declare the set of values a region carries (those it -modifies that are still needed afterward). ``scf`` also has no early exit — a region runs -to its ``scf.yield`` — so Gazprea's ``break``, ``continue``, and ``return`` need a -way to be represented *inside* a structured region; work out how before you commit -to this model. - -.. Warning:: - Avoid emitting unstructured ``cf`` blocks and - placing the phi / block-argument merges yourself. - -.. Warning:: - The SSA model is the more difficult of the two to implement, but can allow for more - optimization to occur. - -.. _ssec:representing_arrays: - -Representing Arrays and Aggregate Types ---------------------------------------- - -Arrays, matrices, and vectors can live in memory or as values, the same choice as -for scalars, but their interaction with tuples/structs is a complication. - -Vector representation -~~~~~~~~~~~~~~~~~~~~~ - -A ``{ptr, len, capacity}`` struct or similar is the standard way to implement growable memory. +``break`` / ``continue`` / ``return`` are ordinary branches with nothing threaded +through them and no analysis of which variables cross a construct. + +*Arrays, matrices, vectors, and aggregates.* Use a ``{ptr, len, cap}`` struct (or +``{ptr, len}`` where capacity is not needed) for **every** sequence: arrays, +vectors, strings, matrices. It is one representation for growable and fixed +sequences alike, and — because it *is* an LLVM type — it nests freely inside an +``!llvm.struct``. A tuple or struct is then just an ``!llvm.struct`` of its fields, +with an array field an ordinary ``{ptr, len}`` member and no special case. Keeping +this single representation across the whole language is the recommended starting +point. .. Note:: For performance marks, consider how your vector grows. -Array representation -~~~~~~~~~~~~~~~~~~~~ - -Three options, differing in ease of implementation and -optimization potential. - -- **A** ``memref`` **(memory model).** Allows optimization via ``linalg.generic``, but - gives up - fusion: a chain like ``a * b + a`` is two loops with an intermediate buffer. - -- **A** ``tensor`` **(value model).** - Element assignment ``v[i] = e`` is a ``tensor.insert`` that *yields a new value* - you rebind, threaded exactly like a scalar. Fusion is available — a pure - elementwise chain of ``tensor`` values fuses into one loop — but it must happen - while the arrays are still ``tensor`` (once bufferized they are memory). - -- **A** ``{ptr, len, cap}`` **struct**: The simplest option, uniform with vectors. - You give - up the standard-dialect machinery — no ``linalg``, no ``memref`` / ``tensor`` - passes. - -Allocation, ownership, and freeing for all three are covered in -:ref:`ssec:backend_memory`. - -Array fields inside structs and tuples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A struct or tuple may have an -array-typed field — ``struct s1 (integer i, real r, integer[10] iv)``, -``tuple(integer, real, integer[10])``. A standalone array can use any of the three above -representations; an array within a struct must exist as a valid representation -inside the aggregate or be lifted out of it. - -.. Warning:: - ``!llvm.struct`` and ``!llvm.struct`` will not lower, only an - LLVM-typed field may nest inside an ``llvm.struct``. A ``{ptr, len, cap}`` - struct *is* an LLVM type and nests freely. - -**Option 1 — one representation everywhere.** Use a ``{ptr, len, cap}`` or ``{ptr, len}`` struct for -**every** sequence: arrays, vectors, strings, matrices. A struct or tuple is then -an ``!llvm.struct`` of its fields, and an array field is just a ``{ptr, len}`` -member — nesting is free and has no special case. - -**Option 2 — split by mutability.** Represent arrays and matrices as -``memref`` or ``tensor``. Because a ``memref`` / ``tensor`` cannot nest in an ``!llvm.struct``, an array that -appears as an aggregate field is handled by **SoA decomposition**: the aggregate is -split into its leaves, and each array field becomes its own top-level ``memref`` / -``tensor`` value threaded alongside the others rather than a member of one struct -object. A ``{i, r, iv}`` struct is carried as the parallel leaves ``i32``, -``f64``, ``memref<10xi32>``; there is no single aggregate value. Vectors, being -``{ptr, len, cap}``, still nest normally. The cost is two sequence representations -plus the decomposition machinery — a 1:N type conversion that splits aggregates -into leaves and carries them across call, return, and loop boundaries. The payoff -is greater array and matrix optimization. - -.. Warning:: - This is lots of work to implement. - -.. Note:: - Under Struct of Arrays (SoA) decomposition, the following transformation occurs: - :: - tuple(integer, real[10], integer[2]) data; - data.2[5] = 4; +The value (SSA) model +~~~~~~~~~~~~~~~~~~~~~ +Understand the formulation, avoid implementing it. - becomes - :: - integer data_1; - real[10] data_2; - integer[2] data_3; - data_2[5] = 4; - - In the resulting program, there are no arrays in aggregates. +*Scalars.* Map each variable name to its *current* SSA value: a read is a lookup, an +assignment produces a new value and rebinds the name. There are no slots and no +loads or stores; bufferization introduces memory later, downstream of your emitter. +The one hard case is a *merge*: after an ``if``, a variable set on only one branch +still needs a single value afterward. +*Arrays and aggregates.* The array analog is ``tensor``: a write yields a new value +you thread through, exactly as in the scalar model and with the same costs. It also +cannot nest in an ``!llvm.struct``, so array-typed struct and tuple fields must be +split out of their aggregate (SROA/aggregate flattening). The payoff is greater array and +matrix optimization. .. _ssec:backend_memory: @@ -179,12 +96,12 @@ Below is an example of how to use ``malloc`` and ``free`` within MLIR using the :: module { - llvm.func @malloc(i32) -> !llvm.ptr - llvm.func @free(!llvm.ptr) + llvm.func @malloc(i64) -> !llvm.ptr + llvm.func @free(!llvm.ptr) llvm.func @main() -> i32 { - %0 = llvm.mlir.constant(128 : i32) : i32 - %1 = llvm.call @malloc(%0) : (i32) -> !llvm.ptr - llvm.call @free(%1) : (!llvm.ptr) -> () + %0 = llvm.mlir.constant(128 : i64) : i64 + %1 = llvm.call @malloc(%0) : (i64) -> !llvm.ptr + llvm.call @free(%1) : (!llvm.ptr) -> () %c0_i32 = llvm.mlir.constant(0 : i32) : i32 llvm.return %c0_i32 : i32 } From 8729666cb5be2853d7ff201536b9c6bd56361ae9 Mon Sep 17 00:00:00 2001 From: Chloe Dancey <22119302+novo52@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:21:16 -0600 Subject: [PATCH 3/5] Changed example to use func dialect --- gazprea/impl/backend.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gazprea/impl/backend.rst b/gazprea/impl/backend.rst index be45471..ad6f59e 100644 --- a/gazprea/impl/backend.rst +++ b/gazprea/impl/backend.rst @@ -96,14 +96,14 @@ Below is an example of how to use ``malloc`` and ``free`` within MLIR using the :: module { - llvm.func @malloc(i64) -> !llvm.ptr - llvm.func @free(!llvm.ptr) - llvm.func @main() -> i32 { - %0 = llvm.mlir.constant(128 : i64) : i64 - %1 = llvm.call @malloc(%0) : (i64) -> !llvm.ptr - llvm.call @free(%1) : (!llvm.ptr) -> () - %c0_i32 = llvm.mlir.constant(0 : i32) : i32 - llvm.return %c0_i32 : i32 + func.func private @malloc(i64) -> !llvm.ptr + func.func private @free(!llvm.ptr) + func.func @main() -> i32 { + %0 = arith.constant 128 : i64 + %1 = func.call @malloc(%0) : (i64) -> !llvm.ptr + func.call @free(%1) : (!llvm.ptr) -> () + %c0_i32 = arith.constant 0 : i32 + func.return %c0_i32 : i32 } } From 56dcb371aacacf875fe9fdcdba96090caa9e2dea Mon Sep 17 00:00:00 2001 From: Chloe Dancey <22119302+novo52@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:48:43 -0600 Subject: [PATCH 4/5] Updated prose preceeding updated example --- gazprea/impl/backend.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gazprea/impl/backend.rst b/gazprea/impl/backend.rst index ad6f59e..b90a262 100644 --- a/gazprea/impl/backend.rst +++ b/gazprea/impl/backend.rst @@ -91,7 +91,7 @@ but this could be problematic if the arrays are very large. It is likely safer to use ``malloc`` and ``free`` for these purposes. This may be done in either your runtime or directly within MLIR. -Below is an example of how to use ``malloc`` and ``free`` within MLIR using the LLVM dialect: +Below is an example of how to use ``malloc`` and ``free`` within MLIR: :: From e78bc8ea47fded9a02d5b874c6fd888411bff372 Mon Sep 17 00:00:00 2001 From: Chloe Dancey <22119302+novo52@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:24:52 -0600 Subject: [PATCH 5/5] Added link between MLIR resources --- gazprea/impl/backend.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gazprea/impl/backend.rst b/gazprea/impl/backend.rst index b90a262..6dad4cf 100644 --- a/gazprea/impl/backend.rst +++ b/gazprea/impl/backend.rst @@ -6,6 +6,10 @@ Backend You don’t need to implement an interpreter for Gazprea. You only need to implement a *MLIR* code generator that outputs *LLVM IR*. +.. This uses a direct link because it links to a different sphinx project (info) + +See also `MLIR Tips and Hints <../../info/mlir_tips.html>`_ for detailed MLIR and dialect tips, debugging strategies, and dialect selection guidance. + .. _ssec:representing_values: Representing Values