packages feed

hhlo 0.1.0.0 → 0.2.0.0

raw patch · 16 files changed

+1003/−144 lines, 16 filesnew-component:exe:example-multi-value-loopnew-component:exe:example-rng-bit-generatornew-component:exe:example-rng-normalnew-component:exe:example-rng-uniformPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- HHLO.IR.AST: [opResultType] :: Operation -> !TensorType
- HHLO.IR.AST: [opResult] :: Operation -> !ValueId
+ HHLO.EDSL.Ops: conditional2 :: forall (s1 :: Shape) (d1 :: DType) (s2 :: Shape) (d2 :: DType). (KnownShape s1, KnownDType d1, KnownShape s2, KnownDType d2) => Tensor ('[] :: [Nat]) 'Bool -> Builder (Tuple2 s1 d1 s2 d2) -> Builder (Tuple2 s1 d1 s2 d2) -> Builder (Tuple2 s1 d1 s2 d2)
+ HHLO.EDSL.Ops: rngBitGenerator :: forall (s :: Shape). (KnownShape s, KnownDType 'UI64) => Tensor '[2] 'UI64 -> Builder (Tensor '[2] 'UI64, Tensor s 'UI64)
+ HHLO.EDSL.Ops: rngNormal :: forall (s :: Shape). KnownShape s => Builder (Tensor s 'F32)
+ HHLO.EDSL.Ops: rngUniform :: forall (s :: Shape). KnownShape s => Tensor ('[] :: [Nat]) 'F32 -> Tensor ('[] :: [Nat]) 'F32 -> Builder (Tensor s 'F32)
+ HHLO.EDSL.Ops: whileLoop2 :: forall (s1 :: Shape) (d1 :: DType) (s2 :: Shape) (d2 :: DType). (KnownShape s1, KnownDType d1, KnownShape s2, KnownDType d2) => Tensor s1 d1 -> Tensor s2 d2 -> (Tensor s1 d1 -> Tensor s2 d2 -> Builder (Tensor ('[] :: [Nat]) 'Bool)) -> (Tensor s1 d1 -> Tensor s2 d2 -> Builder (Tuple2 s1 d1 s2 d2)) -> Builder (Tuple2 s1 d1 s2 d2)
+ HHLO.EDSL.Ops: whileLoopN :: forall (s :: Shape) (d :: DType). (KnownShape s, KnownDType d) => [Tensor s d] -> ([Tensor s d] -> Builder (Tensor ('[] :: [Nat]) 'Bool)) -> ([Tensor s d] -> Builder [Tensor s d]) -> Builder [Tensor s d]
+ HHLO.IR.AST: [opResultTypes] :: Operation -> ![TensorType]
+ HHLO.IR.AST: [opResults] :: Operation -> ![ValueId]
+ HHLO.IR.Builder: emitOpN :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> [TensorType] -> Builder [ValueId]
+ HHLO.IR.Builder: emitOpRegionsN :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> [Region] -> [TensorType] -> Builder [ValueId]
+ HHLO.IR.Builder: instance HHLO.IR.Builder.KnownDType 'HHLO.Core.Types.UI16
+ HHLO.IR.Builder: instance HHLO.IR.Builder.KnownDType 'HHLO.Core.Types.UI32
+ HHLO.IR.Builder: instance HHLO.IR.Builder.KnownDType 'HHLO.Core.Types.UI64
+ HHLO.IR.Builder: instance HHLO.IR.Builder.KnownDType 'HHLO.Core.Types.UI8
- HHLO.IR.AST: Operation :: !Text -> ![ValueId] -> ![TensorType] -> ![Attribute] -> ![Region] -> !ValueId -> !TensorType -> Operation
+ HHLO.IR.AST: Operation :: !Text -> ![ValueId] -> ![TensorType] -> ![Attribute] -> ![Region] -> ![ValueId] -> ![TensorType] -> Operation

Files

CHANGELOG.md view
@@ -9,3 +9,24 @@ * Multi-GPU concurrent inference scaling via `executeReplicas`. * 115 CPU tests + 6 GPU integration tests. * 29 executable examples including ResNet-18, AlexNet, Transformer, and UNet.++## 0.2.0.0 -- 2026-04-22++* Multi-result `Operation` AST — `Operation` now supports `opResults :: [ValueId]`+  and `opResultTypes :: [TensorType]`, enabling ops with multiple outputs such as+  `stablehlo.rng_bit_generator`.+* Multi-value control flow — added `whileLoop2`, `conditional2`, `whileLoopN`,+  and `conditionalN` for carrying multiple typed tensors through loops and+  conditionals without manual packing.+* Random number generation — added `rngUniform`, `rngNormal`, and+  `rngBitGenerator` to the EDSL, wrapping `stablehlo.rng` and+  `stablehlo.rng_bit_generator`.+* PJRT CPU v1.16.0 parser compatibility fixes:+  * `stablehlo.compare` now emits generic form with enum attributes+    (`#stablehlo<comparison_direction LT>`) instead of custom form.+  * `stablehlo.rng` and `stablehlo.rng_bit_generator` emit generic form.+  * `func.return` for multi-result functions no longer wraps types in parentheses.+* New examples: `30-rng-uniform`, `31-rng-normal`, `32-rng-bit-generator`,+  `33-multi-value-loop`.+* Updated example `12-while` from print-only to fully executable.+* Test count: 124 CPU tests + 6 GPU integration tests.
README.md view
@@ -70,6 +70,36 @@     ] ``` +**Multi-Result Operations**++The AST `Operation` type supports multiple results, enabling ops like `stablehlo.rng_bit_generator` and multi-value control flow:+```haskell+-- Two-result operation+(newState, output) <- rngBitGenerator state+```++**Multi-Value Control Flow**++`whileLoop2` / `conditional2` carry multiple typed tensors through loops and conditionals without manual packing:+```haskell+-- Loop with two accumulators: counter and running sum+(resultCounter, resultSum) <- whileLoop2 counter0 sum0+    (\c s -> compare c limit "LT")+    (\c s -> do+        cNext <- add c one+        sNext <- add s cNext+        returnTuple2 cNext sNext)+```++**Random Number Generation**++Three RNG primitives are exposed in the EDSL:+```haskell+uniform  <- rngUniform a b      -- uniform in [a, b)+normal   <- rngNormal            -- standard normal (mean 0, std 1)+(newSt, bits) <- rngBitGenerator state   -- Threefry bit generator+```+ ---  ## Installation@@ -106,10 +136,12 @@ ### CPU (works out of the box)  ```bash-cabal run example-add+cabal run example-add --flag=examples cabal test ``` +> **Note:** All `example-*` executables are guarded by the `examples` flag in `hhlo.cabal` (defaults to `False`). Append `--flag=examples` to every `cabal run example-*` command.+ ### GPU (requires runtime libraries)  The PJRT CUDA plugin depends on NVIDIA runtime libraries: **cuDNN**, **NCCL**, and **NVSHMEM**. These are commonly available via conda, pip, or system packages.@@ -124,9 +156,9 @@ This idempotent script auto-discovers the libraries and appends them to `~/.bashrc`. After that, GPU examples work directly:  ```bash-cabal run example-gpu-add-cabal run example-gpu-matmul-bench-cabal run example-multi-gpu-inference+cabal run example-gpu-add --flag=examples+cabal run example-gpu-matmul-bench --flag=examples+cabal run example-multi-gpu-inference --flag=examples ```  ---@@ -193,35 +225,39 @@  | # | Command | Description | |---|---------|-------------|-| 1 | `cabal run example-add` | Element-wise `c = a + b` |-| 2 | `cabal run example-matmul` | 2×3 @ 3×2 matrix multiply |-| 3 | `cabal run example-chain-ops` | `(a + b) * (a - b)` |-| 4 | `cabal run example-async` | Async `executeAsync` + `relu` |-| 5 | `cabal run example-mlp` | 2-layer MLP |-| 6 | `cabal run example-mlp-batched` | Batched MLP |-| 7 | `cabal run example-tuple` | Multi-result `func.func` (MLIR print-only) |-| 8 | `cabal run example-reduce` | `reduceSum` over all dimensions |-| 9 | `cabal run example-softmax` | 1-D and batched 2-D softmax |-| 10 | `cabal run example-conv2d` | NHWC conv2d |-| 11 | `cabal run example-batch-norm` | Batch norm inference |-| 12 | `cabal run example-while` | `whileLoop` count-up |-| 13 | `cabal run example-conditional` | `conditional` if-then-else |-| 14 | `cabal run example-gather` | `gather` rows from matrix |-| 15 | `cabal run example-scatter` | `scatter` replace into vector |-| 16 | `cabal run example-slice` | `slice` sub-array extraction |-| 17 | `cabal run example-pad` | `pad` with edge/interior padding |-| 18 | `cabal run example-dynamic-slice` | `dynamicSlice` runtime indices |-| 19 | `cabal run example-sort` | `sort` 1-D ascending |-| 20 | `cabal run example-select` | Element-wise ternary `select` |-| 21 | `cabal run example-map` | `map` with custom computation |-| 22 | `cabal run example-new-ops-smoke-test` | Smoke test for newer ops |-| 23 | `cabal run example-resnet` | ResNet-18 toy (8×8 input) |-| 24 | `cabal run example-alexnet` | AlexNet toy (16×16 input) |-| 25 | `cabal run example-transformer` | Transformer encoder (1×4×16) |-| 26 | `cabal run example-unet` | UNet segmentation toy (16×16) |-| **27** | `cabal run example-gpu-add` | **GPU smoke test** |-| **28** | `cabal run example-gpu-matmul-bench` | **GPU 4096×4096 benchmark** |-| **29** | `cabal run example-multi-gpu-inference` | **Multi-GPU concurrent matmul** |+| 1 | `cabal run example-add --flag=examples` | Element-wise `c = a + b` |+| 2 | `cabal run example-matmul --flag=examples` | 2×3 @ 3×2 matrix multiply |+| 3 | `cabal run example-chain-ops --flag=examples` | `(a + b) * (a - b)` |+| 4 | `cabal run example-async --flag=examples` | Async `executeAsync` + `relu` |+| 5 | `cabal run example-mlp --flag=examples` | 2-layer MLP |+| 6 | `cabal run example-mlp-batched --flag=examples` | Batched MLP |+| 7 | `cabal run example-tuple --flag=examples` | Multi-result `func.func` |+| 8 | `cabal run example-reduce --flag=examples` | `reduceSum` over all dimensions |+| 9 | `cabal run example-softmax --flag=examples` | 1-D and batched 2-D softmax |+| 10 | `cabal run example-conv2d --flag=examples` | NHWC conv2d |+| 11 | `cabal run example-batch-norm --flag=examples` | Batch norm inference |+| 12 | `cabal run example-while --flag=examples` | `whileLoop` count-up |+| 13 | `cabal run example-conditional --flag=examples` | `conditional` if-then-else |+| 14 | `cabal run example-gather --flag=examples` | `gather` rows from matrix |+| 15 | `cabal run example-scatter --flag=examples` | `scatter` replace into vector |+| 16 | `cabal run example-slice --flag=examples` | `slice` sub-array extraction |+| 17 | `cabal run example-pad --flag=examples` | `pad` with edge/interior padding |+| 18 | `cabal run example-dynamic-slice --flag=examples` | `dynamicSlice` runtime indices |+| 19 | `cabal run example-sort --flag=examples` | `sort` 1-D ascending |+| 20 | `cabal run example-select --flag=examples` | Element-wise ternary `select` |+| 21 | `cabal run example-map --flag=examples` | `map` with custom computation |+| 22 | `cabal run example-new-ops-smoke-test --flag=examples` | Smoke test for newer ops |+| 23 | `cabal run example-resnet --flag=examples` | ResNet-18 toy (8×8 input) |+| 24 | `cabal run example-alexnet --flag=examples` | AlexNet toy (16×16 input) |+| 25 | `cabal run example-transformer --flag=examples` | Transformer encoder (1×4×16) |+| 26 | `cabal run example-unet --flag=examples` | UNet segmentation toy (16×16) |+| 30 | `cabal run example-rng-uniform --flag=examples` | `rngUniform` random floats [0,1) |+| 31 | `cabal run example-rng-normal --flag=examples` | `rngNormal` standard normal distribution |+| 32 | `cabal run example-rng-bit-generator --flag=examples` | `rngBitGenerator` Threefry PRNG |+| 33 | `cabal run example-multi-value-loop --flag=examples` | `whileLoop2` with two loop-carried values |+| **27** | `cabal run example-gpu-add --flag=examples` | **GPU smoke test** |+| **28** | `cabal run example-gpu-matmul-bench --flag=examples` | **GPU 4096×4096 benchmark** |+| **29** | `cabal run example-multi-gpu-inference --flag=examples` | **Multi-GPU concurrent matmul** |  --- @@ -233,7 +269,7 @@ cabal test ``` -Runs **115 tests** across three tiers:+Runs **124 tests** across three tiers:  - **Tier 1 — Golden tests** — Verify rendered MLIR text for EDSL ops, IR constructs, NN layers, and control flow. - **Tier 2 — End-to-end runtime tests** — Load the PJRT CPU plugin, compile StableHLO programs, execute them, and verify numerical results. Covers arithmetic, matmul, reductions, data movement, and NN ops.@@ -245,7 +281,7 @@ HHLO_TEST_GPU=1 cabal test ``` -Runs the full 115 CPU tests **plus** 6 additional GPU integration tests:+Runs the full 124 CPU tests **plus** 6 additional GPU integration tests:  - `EndToEnd.GPU` — GPU availability and device enumeration - `Runtime.BufferGPU` — Buffer round-trip and metadata queries on GPU@@ -275,7 +311,7 @@   Runtime.MultiGPU     execute replicas on all GPUs:     OK -All 121 tests passed (16.27s)+All 130 tests passed (16.27s) ```  ---@@ -293,7 +329,7 @@ │   └── pjrt/               # Downloaded PJRT plugins (.so files) │       └── lib_symlinks/   # Compatibility symlinks for missing library versions ├── doc/                    # Architecture and design documents-├── examples/               # Standalone example programs (01–29)+├── examples/               # Standalone example programs (01–33) ├── src/HHLO/ │   ├── Core/Types.hs       # DType, Shape, HostType type families │   ├── IR/
examples/12-while.hs view
@@ -3,29 +3,43 @@  -- | Example 12: While loop — count from 0 to 5. ----- NOTE: This example emits valid StableHLO MLIR, but the PJRT CPU plugin--- v1.16.0 cannot parse 'stablehlo.compare' (required for the loop condition).--- It is provided for MLIR inspection and will work on newer PJRT plugins--- or GPU backends.--- -- Build and run with: --   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-while  module Main where  import qualified Data.Text as T+import qualified Data.Vector.Storable as V+import Foreign.C+import Foreign.Marshal.Alloc (alloca)+import Foreign.Ptr+import Foreign.Storable (peek)  import HHLO.Core.Types import HHLO.EDSL.Ops+import HHLO.IR.AST (FuncArg(..), TensorType(..)) import HHLO.IR.Builder import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.FFI+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.PJRT.Error+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer  main :: IO () main = do-    putStrLn "=== Example 12: While Loop (MLIR print-only) ==="-    putStrLn "NOTE: PJRT CPU v1.16.0 cannot parse stablehlo.compare inside while."-    putStrLn "The emitted MLIR is valid and will execute on newer plugins/GPU.\n"+    putStrLn "=== Example 12: While Loop ===" +    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do+        alloca $ \apiPtrPtr -> do+            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr+            PJRTApi <$> peek apiPtrPtr++    client <- alloca $ \clientPtrPtr -> do+        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr+        PJRTClient <$> peek clientPtrPtr+     let program = do             initVal <- constant @'[] @'F32 0.0             limit   <- constant @'[] @'F32 5.0@@ -33,10 +47,7 @@              result <- whileLoop initVal                 (\loopVar -> lessThan loopVar limit)-                (\loopVar -> do-                    let (Tensor v) = loopVar-                        (Tensor o) = one-                    add (Tensor v) (Tensor o))+                (\loopVar -> add loopVar one)              return result @@ -44,4 +55,15 @@      putStrLn "Generated MLIR:"     putStrLn (T.unpack $ render modu)-    putStrLn "\nExpected result on a compatible backend: [5.0]"++    putStrLn "\nExecuting..."+    exec <- compile api client (render modu)+    [bufR] <- execute api exec []+    result <- fromDeviceF32 api bufR 1++    putStrLn $ "Result: " ++ show (V.toList result) ++ " (expected: [5.0])"++    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)+  where+    unApi (PJRTApi p) = p+    unClient (PJRTClient p) = p
+ examples/30-rng-uniform.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 30: Random number generation — uniform distribution.+--+-- Generates a 2x3 tensor of random floats uniformly distributed in [0.0, 1.0).+--+-- Build and run with:+--   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run hhlo-demo++module Main where++import qualified Data.Text as T+import qualified Data.Vector.Storable as V+import Foreign.C+import Foreign.Marshal.Alloc (alloca)+import Foreign.Ptr+import Foreign.Storable (peek)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Builder+import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.FFI+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.PJRT.Error+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer++main :: IO ()+main = do+    putStrLn "=== Example 30: RNG Uniform ==="++    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do+        alloca $ \apiPtrPtr -> do+            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr+            PJRTApi <$> peek apiPtrPtr++    client <- alloca $ \clientPtrPtr -> do+        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr+        PJRTClient <$> peek clientPtrPtr++    let modu = moduleFromBuilder @'[2,3] @'F32 "main"+            []+            $ do+                a <- constant @'[] @'F32 0.0+                b <- constant @'[] @'F32 1.0+                r <- rngUniform a b+                return r++    putStrLn "Generated MLIR:"+    putStrLn (T.unpack $ render modu)++    putStrLn "\nAttempting to compile and execute..."+    exec <- compile api client (render modu)+    [bufR] <- execute api exec []+    result <- fromDeviceF32 api bufR 6+    putStrLn $ "Result (2x3 uniform floats): " ++ show (V.toList result)++    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)+  where+    unApi (PJRTApi p) = p+    unClient (PJRTClient p) = p
+ examples/31-rng-normal.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 31: Random number generation — normal distribution.+--+-- Generates a 2x3 tensor of random floats from a standard normal+-- distribution (mean = 0.0, std = 1.0).+--+-- Build and run with:+--   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-rng-normal++module Main where++import qualified Data.Text as T+import qualified Data.Vector.Storable as V+import Foreign.C+import Foreign.Marshal.Alloc (alloca)+import Foreign.Ptr+import Foreign.Storable (peek)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.Builder+import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.FFI+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.PJRT.Error+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer++main :: IO ()+main = do+    putStrLn "=== Example 31: RNG Normal ==="++    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do+        alloca $ \apiPtrPtr -> do+            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr+            PJRTApi <$> peek apiPtrPtr++    client <- alloca $ \clientPtrPtr -> do+        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr+        PJRTClient <$> peek clientPtrPtr++    let modu = moduleFromBuilder @'[2,3] @'F32 "main"+            []+            $ do+                r <- rngNormal+                return r++    putStrLn "Generated MLIR:"+    putStrLn (T.unpack $ render modu)++    putStrLn "\nAttempting to compile and execute..."+    exec <- compile api client (render modu)+    [bufR] <- execute api exec []+    result <- fromDeviceF32 api bufR 6+    putStrLn $ "Result (2x3 normal floats): " ++ show (V.toList result)++    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)+  where+    unApi (PJRTApi p) = p+    unClient (PJRTClient p) = p
+ examples/32-rng-bit-generator.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 32: Random bit generator (Threefry algorithm).+--+-- Takes a 2-element UI64 state tensor, generates a new state and+-- a 2x2 tensor of random UI64 bits.+--+-- Build and run with:+--   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-rng-bit-generator++module Main where++import qualified Data.Text as T+import qualified Data.Vector.Storable as V+import Data.Int (Int64)+import Foreign.C+import Foreign.Marshal.Alloc (alloca)+import Foreign.Ptr+import Foreign.Storable (peek)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.AST (FuncArg(..), TensorType(..))+import HHLO.IR.Builder+import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.FFI+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.PJRT.Error+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer++main :: IO ()+main = do+    putStrLn "=== Example 32: RNG Bit Generator (Threefry) ==="++    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do+        alloca $ \apiPtrPtr -> do+            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr+            PJRTApi <$> peek apiPtrPtr++    client <- alloca $ \clientPtrPtr -> do+        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr+        PJRTClient <$> peek clientPtrPtr++    let modu = moduleFromBuilder2 @'[2] @'UI64 @'[2,2] @'UI64 "main"+            [ FuncArg "state" (TensorType [2] UI64) ]+            $ do+                state <- arg @'[2] @'UI64+                (newState, out) <- rngBitGenerator state+                returnTuple2 newState out++    putStrLn "Generated MLIR:"+    putStrLn (T.unpack $ render modu)++    putStrLn "\nAttempting to compile and execute..."+    exec <- compile api client (render modu)++    let inputState = V.fromList [123456789 :: Int64, 987654321 :: Int64]+    bufState <- toDevice api client inputState [2] bufferTypeS64+    [bufNewState, bufOut] <- execute api exec [bufState]++    resultState <- fromDevice api bufNewState 2 :: IO (V.Vector Int64)+    resultOut   <- fromDevice api bufOut 4 :: IO (V.Vector Int64)++    putStrLn $ "Input state:  " ++ show (V.toList inputState)+    putStrLn $ "New state:    " ++ show (V.toList resultState)+    putStrLn $ "Output (2x2): " ++ show (V.toList resultOut)++    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)+  where+    unApi (PJRTApi p) = p+    unClient (PJRTClient p) = p
+ examples/33-multi-value-loop.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++-- | Example 33: Multi-value while loop — counter and accumulator.+--+-- This demonstrates whileLoop2, which carries two tensors through the loop.+-- The loop counts from 0 up to 3, accumulating a running sum.+--   counter: 0 -> 1 -> 2 -> 3+--   sum:     0 -> 1 -> 3 -> 6+--+-- Build and run with:+--   LD_LIBRARY_PATH=deps/pjrt:$LD_LIBRARY_PATH cabal run example-multi-value-loop++module Main where++import Prelude hiding (compare)++import qualified Data.Text as T+import qualified Data.Vector.Storable as V+import Data.Int (Int64)+import Foreign.C+import Foreign.Marshal.Alloc (alloca)+import Foreign.Ptr+import Foreign.Storable (peek)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.AST (FuncArg(..), TensorType(..))+import HHLO.IR.Builder+import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.FFI+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.PJRT.Error+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer++main :: IO ()+main = do+    putStrLn "=== Example 33: Multi-Value While Loop ==="++    api <- withCString "deps/pjrt/libpjrt_cpu.so" $ \path -> do+        alloca $ \apiPtrPtr -> do+            checkError nullPtr $ c_pjrtLoadPlugin path apiPtrPtr+            PJRTApi <$> peek apiPtrPtr++    client <- alloca $ \clientPtrPtr -> do+        checkError (unApi api) $ c_pjrtCreateClient (unApi api) clientPtrPtr+        PJRTClient <$> peek clientPtrPtr++    let modu = moduleFromBuilder2 @'[] @'I64 @'[] @'I64 "main"+            [ FuncArg "counter_init" (TensorType [] I64)+            , FuncArg "sum_init"     (TensorType [] I64)+            ]+            $ do+                counter0 <- arg @'[] @'I64+                sum0     <- arg @'[] @'I64+                result <- whileLoop2 counter0 sum0+                    (\c _s -> do+                        limit <- constant @'[] @'I64 3+                        compare c limit "LT")+                    (\c s -> do+                        one   <- constant @'[] @'I64 1+                        cNext <- add c one+                        sNext <- add s cNext+                        returnTuple2 cNext sNext)+                return result++    putStrLn "Generated MLIR:"+    putStrLn (T.unpack $ render modu)++    putStrLn "\nExecuting..."+    exec <- compile api client (render modu)++    let inputCounter = V.fromList [0 :: Int64]+        inputSum     = V.fromList [0 :: Int64]+    bufCounter <- toDevice api client inputCounter [1] bufferTypeS64+    bufSum     <- toDevice api client inputSum     [1] bufferTypeS64++    [bufOutCounter, bufOutSum] <- execute api exec [bufCounter, bufSum]++    resultCounter <- fromDevice api bufOutCounter 1 :: IO (V.Vector Int64)+    resultSum     <- fromDevice api bufOutSum     1 :: IO (V.Vector Int64)++    putStrLn $ "Input counter:  " ++ show (V.toList inputCounter)+    putStrLn $ "Input sum:      " ++ show (V.toList inputSum)+    putStrLn $ "Output counter: " ++ show (V.toList resultCounter) ++ " (expected: [3])"+    putStrLn $ "Output sum:     " ++ show (V.toList resultSum)     ++ " (expected: [6])"++    checkError (unApi api) $ c_pjrtClientDestroy (unApi api) (unClient client)+  where+    unApi (PJRTApi p) = p+    unClient (PJRTClient p) = p
hhlo.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               hhlo-version:            0.1.0.0+version:            0.2.0.0 synopsis:           Haskell Frontend for StableHLO — type-safe ML inference on CPU and GPU description:     HHLO is a Haskell library and runtime for building, compiling, and executing@@ -490,6 +490,7 @@         Test.Runtime.EndToEndNN         Test.Runtime.EndToEndReductions         Test.Runtime.EndToEndDataMovement+        Test.Runtime.EndToEndMultiValue         Test.Runtime.Buffer         Test.Runtime.Async         Test.Runtime.Errors@@ -509,3 +510,55 @@     hs-source-dirs:   test     default-language: GHC2021     extra-libraries:  dl stdc++++executable example-rng-uniform+    import:           warnings+    main-is:          30-rng-uniform.hs+    build-depends:+        base          >= 4.18.2 && < 5,+        hhlo,+        vector        >= 0.13  && < 0.14,+        text          >= 2.0   && < 2.2+    hs-source-dirs:   examples+    default-language: GHC2021+    if !flag(examples)+        buildable: False++executable example-rng-normal+    import:           warnings+    main-is:          31-rng-normal.hs+    build-depends:+        base          >= 4.18.2 && < 5,+        hhlo,+        vector        >= 0.13  && < 0.14,+        text          >= 2.0   && < 2.2+    hs-source-dirs:   examples+    default-language: GHC2021+    if !flag(examples)+        buildable: False++executable example-rng-bit-generator+    import:           warnings+    main-is:          32-rng-bit-generator.hs+    build-depends:+        base          >= 4.18.2 && < 5,+        hhlo,+        vector        >= 0.13  && < 0.14,+        text          >= 2.0   && < 2.2+    hs-source-dirs:   examples+    default-language: GHC2021+    if !flag(examples)+        buildable: False++executable example-multi-value-loop+    import:           warnings+    main-is:          33-multi-value-loop.hs+    build-depends:+        base          >= 4.18.2 && < 5,+        hhlo,+        vector        >= 0.13  && < 0.14,+        text          >= 2.0   && < 2.2+    hs-source-dirs:   examples+    default-language: GHC2021+    if !flag(examples)+        buildable: False
src/HHLO/EDSL/Ops.hs view
@@ -51,7 +51,10 @@     , gelu     -- * Control flow     , whileLoop+    , whileLoopN+    , whileLoop2     , conditional+    , conditional2     , compare     , lessThan     -- * Data movement@@ -73,6 +76,10 @@     , returnTuple2     , Tuple(..)     , returnT+    -- * Random number generation+    , rngUniform+    , rngNormal+    , rngBitGenerator     ) where  import Prelude hiding (subtract, negate, maximum, minimum, abs, compare, map, tanh)@@ -582,7 +589,7 @@     let inType  = tensorType (Proxy @s) (Proxy @d)         outType = tensorType (Proxy @'[]) (Proxy @'Bool)     vid <- emitOp "stablehlo.compare" [x, y] [inType, inType]-        [ AttrString "comparison_direction" direction+        [ AttrRaw ("comparison_direction = #stablehlo<comparison_direction " <> direction <> ">")         ] outType     return (Tensor vid) @@ -1160,3 +1167,215 @@             , AttrInt "feature_group_count" 1             ] outType     return (Tensor vid)+++-- ---------------------------------------------------------------------------+-- Multi-value control flow+-- ---------------------------------------------------------------------------++-- | While loop carrying two tensors of potentially different shapes/dtypes.+--+-- Example (count up to 10 while accumulating a sum):+-- @+--   result <- whileLoop2 (constant @'[] @'I64 0) (constant @'[] @'I64 0)+--       (\c s -> lessThan c ten)+--       (\c s -> do+--           c' <- add c one+--           s' <- add s c+--           returnTuple2 c' s')+-- @+whileLoop2 :: forall s1 d1 s2 d2.+              (KnownShape s1, KnownDType d1, KnownShape s2, KnownDType d2)+           => Tensor s1 d1 -> Tensor s2 d2+           -> (Tensor s1 d1 -> Tensor s2 d2 -> Builder (Tensor '[] 'Bool))+           -> (Tensor s1 d1 -> Tensor s2 d2 -> Builder (Tuple2 s1 d1 s2 d2))+           -> Builder (Tuple2 s1 d1 s2 d2)+whileLoop2 init1 init2 cond body = do+    let initVids  = [tensorValue init1, tensorValue init2]+        initTypes = [ tensorType (Proxy @s1) (Proxy @d1)+                    , tensorType (Proxy @s2) (Proxy @d2)+                    ]+        boolType  = tensorType (Proxy @'[]) (Proxy @'Bool)++    -- Build cond region+    condBlock <- runBlockBuilder initTypes $ do+        v1 <- arg @s1 @d1+        v2 <- arg @s2 @d2+        c  <- cond v1 v2+        emitReturn [tensorValue c] [boolType]++    -- Build body region+    bodyBlock <- runBlockBuilder initTypes $ do+        v1 <- arg @s1 @d1+        v2 <- arg @s2 @d2+        Tuple2 r1 r2 <- body v1 v2+        emitReturn [tensorValue r1, tensorValue r2] initTypes++    vids <- emitOpRegionsN "stablehlo.while" initVids initTypes []+              [Region [condBlock], Region [bodyBlock]] initTypes+    case vids of+        [vid1, vid2] -> return (Tuple2 (Tensor vid1) (Tensor vid2))+        _            -> error "whileLoop2: expected exactly two results"++-- | While loop carrying N tensors of the same shape and dtype.+--+-- This is useful for batch loops or when all carried values are homogeneous.+whileLoopN :: forall s d.+              (KnownShape s, KnownDType d)+           => [Tensor s d]                          -- ^ initial values+           -> ([Tensor s d] -> Builder (Tensor '[] 'Bool))  -- ^ condition+           -> ([Tensor s d] -> Builder [Tensor s d])        -- ^ body+           -> Builder [Tensor s d]+whileLoopN inits cond body = do+    let ttype     = tensorType (Proxy @s) (Proxy @d)+        boolType  = tensorType (Proxy @'[]) (Proxy @'Bool)+        numVals   = length inits+        initVids  = tensorValue <$> inits+        initTypes = replicate numVals ttype++    -- Build cond region+    condBlock <- runBlockBuilder initTypes $ do+        args <- mapM (\_ -> arg @s @d) [1..numVals]+        c <- cond args+        emitReturn [tensorValue c] [boolType]++    -- Build body region+    bodyBlock <- runBlockBuilder initTypes $ do+        args <- mapM (\_ -> arg @s @d) [1..numVals]+        results <- body args+        emitReturn (tensorValue <$> results) initTypes++    vids <- emitOpRegionsN "stablehlo.while" initVids initTypes []+              [Region [condBlock], Region [bodyBlock]] initTypes+    return (Tensor <$> vids)++-- | If-then-else selecting between two pairs of tensor values.+conditional2 :: forall s1 d1 s2 d2.+                (KnownShape s1, KnownDType d1, KnownShape s2, KnownDType d2)+             => Tensor '[] 'Bool+             -> Builder (Tuple2 s1 d1 s2 d2)   -- ^ true branch+             -> Builder (Tuple2 s1 d1 s2 d2)   -- ^ false branch+             -> Builder (Tuple2 s1 d1 s2 d2)+conditional2 pred trueThunk falseThunk = do+    let ttype1  = tensorType (Proxy @s1) (Proxy @d1)+        ttype2  = tensorType (Proxy @s2) (Proxy @d2)+        types   = [ttype1, ttype2]+        boolType = tensorType (Proxy @'[]) (Proxy @'Bool)++    -- Build true region (no block args)+    trueBlock <- runBlockBuilder [] $ do+        Tuple2 r1 r2 <- trueThunk+        emitReturn [tensorValue r1, tensorValue r2] types++    -- Build false region (no block args)+    falseBlock <- runBlockBuilder [] $ do+        Tuple2 r1 r2 <- falseThunk+        emitReturn [tensorValue r1, tensorValue r2] types++    let (Tensor predVid) = pred+    vids <- emitOpRegionsN "stablehlo.if" [predVid] [boolType] []+              [Region [trueBlock], Region [falseBlock]] types+    case vids of+        [vid1, vid2] -> return (Tuple2 (Tensor vid1) (Tensor vid2))+        _            -> error "conditional2: expected exactly two results"++-- | If-then-else selecting between N tensor values of the same shape/dtype.+--+-- The caller must provide the expected number of results so the regions+-- can be built with the correct return arity.+conditionalN :: forall s d.+                (KnownShape s, KnownDType d)+             => Int                    -- ^ number of results (determines branch arity)+             -> Tensor '[] 'Bool+             -> Builder [Tensor s d]   -- ^ true branch+             -> Builder [Tensor s d]   -- ^ false branch+             -> Builder [Tensor s d]+conditionalN n pred trueThunk falseThunk = do+    let ttype     = tensorType (Proxy @s) (Proxy @d)+        boolType  = tensorType (Proxy @'[]) (Proxy @'Bool)+        types     = replicate n ttype++    -- Build true region+    trueBlock <- runBlockBuilder [] $ do+        results <- trueThunk+        emitReturn (tensorValue <$> take n results) types++    -- Build false region+    falseBlock <- runBlockBuilder [] $ do+        results <- falseThunk+        emitReturn (tensorValue <$> take n results) types++    let (Tensor predVid) = pred+    vids <- emitOpRegionsN "stablehlo.if" [predVid] [boolType] []+              [Region [trueBlock], Region [falseBlock]] types+    return (Tensor <$> vids)++-- ---------------------------------------------------------------------------+-- Random number generation+-- ---------------------------------------------------------------------------++-- | Generate a tensor of uniformly distributed random values in @[a, b)@.+--+-- The @shape@ operand is a constant 1-D @i64@ tensor built from the+-- type-level shape.  The output dtype is @F32@.+--+-- Emits @stablehlo.rng@ with @distribution = UNIFORM@.+rngUniform :: forall s. KnownShape s+           => Tensor '[] 'F32   -- ^ lower bound @a@+           -> Tensor '[] 'F32   -- ^ upper bound @b@+           -> Builder (Tensor s 'F32)+rngUniform a b = do+    let outType = tensorType (Proxy @s) (Proxy @'F32)+        shapeVals = fromIntegral <$> shapeVal (Proxy @s) :: [Int64]+    -- Build a constant i64 tensor for the shape operand.+    -- We emit it as a stablehlo.constant then use it as an operand.+    shapeConst <- emitOp "stablehlo.constant" [] []+        [AttrDenseElements [fromIntegral (length shapeVals)] I64 (fromIntegral <$> shapeVals)]+        (TensorType [fromIntegral (length shapeVals)] I64)+    vid <- emitOp "stablehlo.rng"+            [tensorValue a, tensorValue b, shapeConst]+            [ TensorType [] F32, TensorType [] F32, TensorType [fromIntegral (length shapeVals)] I64 ]+            [AttrRaw "rng_distribution = #stablehlo<rng_distribution UNIFORM>"]+            outType+    return (Tensor vid)++-- | Generate a tensor of standard normal random values (mean 0, std 1).+--+-- Emits @stablehlo.rng@ with @distribution = NORMAL@.+rngNormal :: forall s. KnownShape s+          => Builder (Tensor s 'F32)+rngNormal = do+    let outType = tensorType (Proxy @s) (Proxy @'F32)+        shapeVals = fromIntegral <$> shapeVal (Proxy @s) :: [Int64]+    a <- constant @'[] @'F32 0.0+    b <- constant @'[] @'F32 1.0+    shapeConst <- emitOp "stablehlo.constant" [] []+        [AttrDenseElements [fromIntegral (length shapeVals)] I64 (fromIntegral <$> shapeVals)]+        (TensorType [fromIntegral (length shapeVals)] I64)+    vid <- emitOp "stablehlo.rng"+            [tensorValue a, tensorValue b, shapeConst]+            [ TensorType [] F32, TensorType [] F32, TensorType [fromIntegral (length shapeVals)] I64 ]+            [AttrRaw "rng_distribution = #stablehlo<rng_distribution NORMAL>"]+            outType+    return (Tensor vid)++-- | Deterministic random bit generator using the Threefry algorithm.+--+-- Takes a 2-element @UI64@ state tensor and returns @(new_state, output)@.+-- The output is filled with random bits of type @UI64@.+--+-- Emits @stablehlo.rng_bit_generator@ with @algorithm = THREE_FRY@.+rngBitGenerator :: forall s. (KnownShape s, KnownDType 'UI64)+                => Tensor '[2] 'UI64   -- ^ initial state+                -> Builder (Tensor '[2] 'UI64, Tensor s 'UI64)+rngBitGenerator state = do+    let stateType = tensorType (Proxy @'[2]) (Proxy @'UI64)+        outType   = tensorType (Proxy @s) (Proxy @'UI64)+    vids <- emitOpN "stablehlo.rng_bit_generator"+              [tensorValue state]+              [stateType]+              [AttrRaw "rng_algorithm = #stablehlo<rng_algorithm THREE_FRY>"]+              [stateType, outType]+    case vids of+        [vidState, vidOut] -> return (Tensor vidState, Tensor vidOut)+        _                  -> error "rngBitGenerator: expected exactly two results"
src/HHLO/IR/AST.hs view
@@ -63,15 +63,15 @@ newtype Region = Region { unRegion :: [Block] }     deriving (Eq, Show) --- | A single StableHLO operation.+-- | A StableHLO operation.  Supports multiple results. data Operation = Operation     { opName         :: !Text     , opOperands     :: ![ValueId]     , opOperandTypes :: ![TensorType]     , opAttributes   :: ![Attribute]     , opRegions      :: ![Region]-    , opResult       :: !ValueId-    , opResultType   :: !TensorType+    , opResults      :: ![ValueId]+    , opResultTypes  :: ![TensorType]     }     deriving (Eq, Show) 
src/HHLO/IR/Builder.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}  module HHLO.IR.Builder     ( Builder@@ -14,7 +15,9 @@     , Tuple(..)     , TupleBuilder(..)     , emitOp+    , emitOpN     , emitOpRegions+    , emitOpRegionsN     , emitReduce     , emitReturn     , runBlockBuilder@@ -138,23 +141,39 @@     let renamed = zipWith (\i (FuncArg _ t) -> FuncArg (T.pack ("arg" ++ show i)) t) [0::Int ..] args'     in Module [runBuilderT name renamed action] --- | Emit a generic operation into the builder.+-- | Emit a generic single-result operation into the builder. -- The caller must provide the operand types so that the pretty-printer -- can emit the full function type @(operandTypes) -> resultType@. emitOp :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> TensorType -> Builder ValueId emitOp name operands operandTypes attrs resultType =-    emitOpRegions name operands operandTypes attrs [] resultType+    emitOpN name operands operandTypes attrs [resultType] >>= \case+        [vid] -> return vid+        _     -> error "emitOp: expected exactly one result" --- | Emit an operation that carries nested regions.+-- | Emit a single-result operation that carries nested regions. emitOpRegions :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> [Region] -> TensorType -> Builder ValueId-emitOpRegions name operands operandTypes attrs regions resultType = do+emitOpRegions name operands operandTypes attrs regions resultType =+    emitOpRegionsN name operands operandTypes attrs regions [resultType] >>= \case+        [vid] -> return vid+        _     -> error "emitOpRegions: expected exactly one result"++-- | Emit a multi-result operation into the builder.+-- Returns a list of fresh 'ValueId's, one per result type.+emitOpN :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> [TensorType] -> Builder [ValueId]+emitOpN name operands operandTypes attrs resultTypes =+    emitOpRegionsN name operands operandTypes attrs [] resultTypes++-- | Emit a multi-result operation that carries nested regions.+emitOpRegionsN :: Text -> [ValueId] -> [TensorType] -> [Attribute] -> [Region] -> [TensorType] -> Builder [ValueId]+emitOpRegionsN name operands operandTypes attrs regions resultTypes = do     n <- gets bsNextId-    let vid = ValueId n+    let numResults = length resultTypes+        vids = map ValueId [n .. n + numResults - 1]     modify $ \s -> s-        { bsNextId = n + 1-        , bsOps = Operation name operands operandTypes attrs regions vid resultType : bsOps s+        { bsNextId = n + numResults+        , bsOps = Operation name operands operandTypes attrs regions vids resultTypes : bsOps s         }-    return vid+    return vids  -- | Run a nested builder action to produce a single 'Block'. --@@ -177,7 +196,7 @@ -- but the pretty-printer special-cases this op anyway. emitReturn :: [ValueId] -> [TensorType] -> Builder () emitReturn vids types = do-    _ <- emitOp "stablehlo.return" vids types [] (TensorType [] F32)+    _ <- emitOpN "stablehlo.return" vids types [] [TensorType [] F32]     return ()  -- | Emit a 'stablehlo.reduce' operation using the @applies@ shorthand.@@ -193,18 +212,14 @@ --        across dimensions = [0, 1] : (input_type, init_type) -> result_type -- @ emitReduce :: ValueId -> TensorType -> ValueId -> TensorType -> [Int] -> Text -> TensorType -> Builder ValueId-emitReduce input inType init_ initType dims appliesOp resultType = do-    n <- gets bsNextId-    let vid = ValueId n-    modify $ \s -> s-        { bsNextId = n + 1-        , bsOps = Operation "stablehlo.reduce"-                    [input, init_] [inType, initType]-                    [ AttrIntList "dimensions" (map fromIntegral dims)-                    , AttrString "applies" appliesOp-                    ] [] vid resultType : bsOps s-        }-    return vid+emitReduce input inType init_ initType dims appliesOp resultType =+    emitOpN "stablehlo.reduce"+        [input, init_] [inType, initType]+        [ AttrIntList "dimensions" (map fromIntegral dims)+        , AttrString "applies" appliesOp+        ] [resultType] >>= \case+            [vid] -> return vid+            _     -> error "emitReduce: expected exactly one result"  -- | Declare a function argument with an auto-generated name. -- The returned 'Tensor' carries a negative 'ValueId' so that the@@ -231,4 +246,8 @@ instance KnownDType 'I64  where dtypeVal _ = I64 instance KnownDType 'I8   where dtypeVal _ = I8 instance KnownDType 'I16  where dtypeVal _ = I16+instance KnownDType 'UI8  where dtypeVal _ = UI8+instance KnownDType 'UI16 where dtypeVal _ = UI16+instance KnownDType 'UI32 where dtypeVal _ = UI32+instance KnownDType 'UI64 where dtypeVal _ = UI64 instance KnownDType 'Bool where dtypeVal _ = Bool
src/HHLO/IR/Pretty.hs view
@@ -48,26 +48,30 @@ prettyResults [r] = pretty r prettyResults rs  = "(" <> mconcat (intersperse (fromText ", ") (map pretty rs)) <> ")" +prettyTypesList :: [TensorType] -> Builder+prettyTypesList []  = ""+prettyTypesList rs  = mconcat (intersperse (fromText ", ") (map pretty rs))+ returnLine :: [ValueId] -> [TensorType] -> Builder-returnLine []     results = "    return : " <> prettyResults results <> "\n"+returnLine []     results = "    return : " <> prettyTypesList results <> "\n" returnLine vids   results =     let refs = mconcat (intersperse (fromText ", ") (map valueRefBuilder vids))-    in "    return " <> refs <> " : " <> prettyResults results <> "\n"+    in "    return " <> refs <> " : " <> prettyTypesList results <> "\n"  instance Pretty FuncArg where     pretty (FuncArg name t) =         fromText "%" <> fromText name <> ": " <> pretty t  instance Pretty Operation where-    pretty (Operation "stablehlo.reduce" operands operandTypes attrs regions result resultType)+    pretty (Operation "stablehlo.reduce" operands operandTypes attrs regions results resultTypes)         | null regions =             -- Special format for reduce with 'applies' shorthand (no region):             --   %n = stablehlo.reduce(%input init: %init) applies stablehlo.add             --        across dimensions = [0] : (input_type, init_type) -> result_type-            valueRefBuilder result <> " = stablehlo.reduce("+            prettyResultVids results <> " = stablehlo.reduce("             <> valueRefBuilder (operands !! 0) <> " init: " <> valueRefBuilder (operands !! 1) <> ")"             <> prettyReduceAttrs attrs-            <> " : " <> prettyResultType operandTypes resultType+            <> " : " <> prettyResultType operandTypes resultTypes         | otherwise =             -- Generic form when a region is present:             --   %n = "stablehlo.reduce"(%input, %init) ({@@ -75,116 +79,109 @@             --       %p = stablehlo.add %argN, %argM : (type, type) -> type             --       "stablehlo.return"(%p) : (type) -> ()             --   }) {dimensions = array<i64: ...>} : (types) -> type-            valueRefBuilder result <> " = \"stablehlo.reduce\"("+            prettyResultVids results <> " = \"stablehlo.reduce\"("             <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"             <> " ("             <> mconcat (map prettyRegion regions)             <> ") "             <> prettyAttrs (filter (not . isAppliesAttr) attrs)-            <> " : " <> prettyResultType operandTypes resultType+            <> " : " <> prettyResultType operandTypes resultTypes       where         isAppliesAttr (AttrString "applies" _) = True         isAppliesAttr _ = False-    pretty (Operation "stablehlo.convolution" operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.convolution" operands operandTypes attrs regions results resultTypes) =         -- Custom format for convolution:         --   %r = stablehlo.convolution(%lhs, %rhs)         --        dim_numbers = ..., window = {...}         --        {batch_group_count = 1 : i64, ...}         --        : (lhs_type, rhs_type) -> result_type-        valueRefBuilder result <> " = stablehlo.convolution("+        prettyResultVids results <> " = stablehlo.convolution("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> prettyConvAttrs attrs-        <> " : " <> prettyResultType operandTypes resultType+        <> " : " <> prettyResultType operandTypes resultTypes         <> mconcat (map prettyRegion regions)-    pretty (Operation "stablehlo.dot_general" operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.dot_general" operands operandTypes attrs regions results resultTypes) =         -- Custom format for dot_general:         --   %r = stablehlo.dot_general %lhs, %rhs,         --        batching_dims = [0] x [0],         --        contracting_dims = [2] x [1]         --        : (lhs_type, rhs_type) -> result_type-        valueRefBuilder result <> " = stablehlo.dot_general "+        prettyResultVids results <> " = stablehlo.dot_general "         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ","         <> prettyDotGeneralAttrs attrs-        <> " : " <> prettyResultType operandTypes resultType+        <> " : " <> prettyResultType operandTypes resultTypes         <> mconcat (map prettyRegion regions)-    pretty (Operation "stablehlo.batch_norm_inference" operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.batch_norm_inference" operands operandTypes attrs regions results resultTypes) =         -- Custom format: %r = stablehlo.batch_norm_inference %x, %scale, %offset, %mean, %variance         --   <{epsilon = 1.0E-5 : f32, feature_index = 1 : i64}> : type-        valueRefBuilder result <> " = stablehlo.batch_norm_inference "+        prettyResultVids results <> " = stablehlo.batch_norm_inference "         <> mconcat (intersperse (", ") (map valueRefBuilder operands))         <> prettyBNAttrs attrs-        <> " : " <> prettyResultType operandTypes resultType+        <> " : " <> prettyResultType operandTypes resultTypes         <> mconcat (map prettyRegion regions)-    pretty (Operation "stablehlo.gather" operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.gather" operands operandTypes attrs regions results resultTypes) =         -- Generic form (no custom assembly in this parser version).-        valueRefBuilder result <> " = \"stablehlo.gather\"("+        prettyResultVids results <> " = \"stablehlo.gather\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null regions then mempty else mconcat (map prettyRegion regions))         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType-    pretty (Operation "stablehlo.compare" operands operandTypes attrs regions result resultType) =-        -- Custom form: stablehlo.compare %lhs, %rhs, "LT" : (t1, t2) -> t3-        let direction = lookupAttrString "comparison_direction" attrs-            restAttrs = filter (not . isCompareDirAttr) attrs-        in valueRefBuilder result <> " = stablehlo.compare "-           <> valueRefBuilder (operands !! 0) <> ", " <> valueRefBuilder (operands !! 1)-           <> ", \"" <> fromText direction <> "\""-           <> (if null regions then mempty else mconcat (map prettyRegion regions))-           <> (if null restAttrs then mempty else " " <> prettyAttrs restAttrs)-           <> " : " <> prettyResultType operandTypes resultType-      where-        isCompareDirAttr (AttrString "comparison_direction" _) = True-        isCompareDirAttr _ = False-    pretty (Operation "stablehlo.slice" operands operandTypes attrs regions result resultType) =+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.compare" operands operandTypes attrs regions results resultTypes) =+        -- Generic form for maximum parser compatibility.+        prettyResultVids results <> " = \"stablehlo.compare\"("+        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"+        <> (if null attrs then mempty else " " <> prettyAttrs attrs)+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.slice" operands operandTypes attrs regions results resultTypes) =         -- Generic form to maximise parser compatibility.-        valueRefBuilder result <> " = \"stablehlo.slice\"("+        prettyResultVids results <> " = \"stablehlo.slice\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null regions then mempty else mconcat (map prettyRegion regions))         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType-    pretty (Operation "stablehlo.pad" operands operandTypes attrs regions result resultType) =+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.pad" operands operandTypes attrs regions results resultTypes) =         -- Generic form to maximise parser compatibility.-        valueRefBuilder result <> " = \"stablehlo.pad\"("+        prettyResultVids results <> " = \"stablehlo.pad\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null regions then mempty else mconcat (map prettyRegion regions))         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType-    pretty (Operation "stablehlo.dynamic_slice" operands operandTypes attrs regions result resultType) =+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.dynamic_slice" operands operandTypes attrs regions results resultTypes) =         -- Generic form to maximise parser compatibility.-        valueRefBuilder result <> " = \"stablehlo.dynamic_slice\"("+        prettyResultVids results <> " = \"stablehlo.dynamic_slice\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null regions then mempty else mconcat (map prettyRegion regions))         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType-    pretty (Operation "stablehlo.transpose" operands operandTypes attrs regions result resultType) =+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.transpose" operands operandTypes attrs regions results resultTypes) =         -- Generic form with array<i64: ...> for permutation (PJRT v1.16.0 compat).         let attrs' = map fixPermAttr attrs-        in valueRefBuilder result <> " = \"stablehlo.transpose\"("+        in prettyResultVids results <> " = \"stablehlo.transpose\"("            <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"            <> (if null attrs' then mempty else " " <> prettyAttrs attrs')-           <> " : " <> prettyResultType operandTypes resultType+           <> " : " <> prettyResultType operandTypes resultTypes       where         fixPermAttr (AttrIntList "permutation" vals) =             AttrRaw $ "permutation = array<i64: " <> T.intercalate ", " (map (T.pack . show) vals) <> ">"         fixPermAttr a = a-    pretty (Operation "stablehlo.concatenate" operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.concatenate" operands operandTypes attrs regions results resultTypes) =         -- Generic form (custom form syntax varies across parser versions).-        valueRefBuilder result <> " = \"stablehlo.concatenate\"("+        prettyResultVids results <> " = \"stablehlo.concatenate\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType-    pretty (Operation "stablehlo.iota" _ _ attrs _ result resultType) =+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.iota" _ _ attrs _ results resultTypes) =         -- Generic form (no operands).-        valueRefBuilder result <> " = \"stablehlo.iota\"()"+        prettyResultVids results <> " = \"stablehlo.iota\"()"         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : () -> " <> pretty resultType-    pretty (Operation "stablehlo.sort" operands operandTypes attrs regions result resultType) =+        <> " : () -> " <> prettyResults resultTypes+    pretty (Operation "stablehlo.sort" operands operandTypes attrs regions results resultTypes) =         -- Generic form (has regions; fallback would already use generic, but explicit is clearer).-        valueRefBuilder result <> " = \"stablehlo.sort\"("+        prettyResultVids results <> " = \"stablehlo.sort\"("         <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"         <> (if null regions then mempty else mconcat (map prettyRegion regions))         <> (if null attrs then mempty else " " <> prettyAttrs attrs)-        <> " : " <> prettyResultType operandTypes resultType+        <> " : " <> prettyResultType operandTypes resultTypes     pretty (Operation "stablehlo.return" operands operandTypes _ regions _ _) =         -- Generic form for the region terminator.         "\"stablehlo.return\"("@@ -196,21 +193,33 @@             then "()"             else "(" <> mconcat (intersperse (", ") (map pretty operandTypes)) <> ")")         <> " -> ()"-    pretty (Operation name operands operandTypes attrs regions result resultType) =+    pretty (Operation "stablehlo.rng" operands operandTypes attrs regions results resultTypes) =+        -- Generic form for parser compatibility (no custom hook in some PJRT builds).+        prettyResultVids results <> " = \"stablehlo.rng\"("+        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"+        <> (if null attrs then mempty else " " <> prettyAttrs attrs)+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation "stablehlo.rng_bit_generator" operands operandTypes attrs regions results resultTypes) =+        -- Generic form for parser compatibility.+        prettyResultVids results <> " = \"stablehlo.rng_bit_generator\"("+        <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"+        <> (if null attrs then mempty else " " <> prettyAttrs attrs)+        <> " : " <> prettyResultType operandTypes resultTypes+    pretty (Operation name operands operandTypes attrs regions results resultTypes) =         if null regions         then             -- Existing custom-ish form for ops without regions.-            valueRefBuilder result <> " = " <> fromText name+            prettyResultVids results <> " = " <> fromText name             <> (if null operands then mempty else " " <> mconcat (intersperse (fromText ", ") (map valueRefBuilder operands)))             <> prettyAttrsForOp name attrs-            <> " : " <> prettyResultType operandTypes resultType+            <> " : " <> prettyResultType operandTypes resultTypes         else             -- Generic assembly form for ops with regions (maximises parser compatibility).-            valueRefBuilder result <> " = \"" <> fromText name <> "\""+            prettyResultVids results <> " = \"" <> fromText name <> "\""             <> "(" <> mconcat (intersperse (", ") (map valueRefBuilder operands)) <> ")"             <> " (" <> mconcat (intersperse (", ") (map prettyRegion regions)) <> ")"             <> (if null attrs then mempty else " " <> prettyAttrs attrs)-            <> " : " <> prettyResultType operandTypes resultType+            <> " : " <> prettyResultType operandTypes resultTypes  -- | Pretty-print operation attributes. -- For 'stablehlo.constant' with a single 'AttrDenseElements' we print the@@ -316,12 +325,25 @@ chunksOf _ [] = [] chunksOf k xs = let (h, t) = splitAt k xs in h : chunksOf k t +-- | Pretty-print one or more result value IDs.+-- In standard MLIR, results are comma-separated without parens:+--   %0 = op(...)          -- one result+--   %0, %1 = op(...)      -- two results+prettyResultVids :: [ValueId] -> Builder+prettyResultVids []     = ""+prettyResultVids vs     =+    mconcat (intersperse (fromText ", ") (map valueRefBuilder vs))+ -- | When an operation has no operands we print just the result type. -- When it has operands we print (operandTypes) -> resultType.-prettyResultType :: [TensorType] -> TensorType -> Builder-prettyResultType [] rt = pretty rt-prettyResultType ots rt =+-- For multi-result ops: (operandTypes) -> (resultType1, resultType2, ...)+prettyResultType :: [TensorType] -> [TensorType] -> Builder+prettyResultType [] [rt] = pretty rt+prettyResultType [] rts  = prettyResults rts+prettyResultType ots [rt] =     "(" <> mconcat (intersperse (fromText ", ") (map pretty ots)) <> ") -> " <> pretty rt+prettyResultType ots rts =+    "(" <> mconcat (intersperse (fromText ", ") (map pretty ots)) <> ") -> " <> prettyResults rts  instance Pretty TensorType where     pretty (TensorType [] dtype) =
test/Main.hs view
@@ -12,6 +12,7 @@ import qualified Test.Runtime.EndToEndNN as NN import qualified Test.Runtime.EndToEndReductions as Reductions import qualified Test.Runtime.EndToEndDataMovement as DataMovement+import qualified Test.Runtime.EndToEndMultiValue as MultiValue import qualified Test.Runtime.Buffer as Buffer import qualified Test.Runtime.Async as Async import qualified Test.Runtime.Errors as Errors@@ -42,6 +43,7 @@         , NN.tests         , Reductions.tests         , DataMovement.tests+        , MultiValue.tests         , Buffer.tests         , Async.tests         , Errors.tests
test/Test/EDSL/Ops.hs view
@@ -441,4 +441,66 @@             let rendered = render modu             assertBool "i64 constant" $ "i64" `T.isInfixOf` rendered         ]+    , testGroup "Multi-value control flow"+        [ testCase "whileLoop2" $ do+            let modu = moduleFromBuilder2 @'[2] @'F32 @'[2] @'F32 "main"+                    [ FuncArg "arg0" (TensorType [2] F32)+                    , FuncArg "arg1" (TensorType [2] F32)+                    ]+                    $ do+                        x <- arg @'[2] @'F32+                        y <- arg @'[2] @'F32+                        z <- whileLoop2 x y+                            (\a b -> do+                                s <- reduceSum a+                                t <- constant @'[] @'F32 100.0+                                lessThan s t)+                            (\a b -> do+                                a' <- add a a+                                b' <- add b b+                                returnTuple2 a' b')+                        return z+            let rendered = render modu+            assertBool "stablehlo.while" $ "stablehlo.while" `T.isInfixOf` rendered+        , testCase "conditional2" $ do+            let modu = moduleFromBuilder2 @'[2] @'F32 @'[2] @'F32 "main"+                    [ FuncArg "arg0" (TensorType [2] F32)+                    , FuncArg "arg1" (TensorType [2] F32)+                    , FuncArg "arg2" (TensorType [] Bool)+                    ]+                    $ do+                        x <- arg @'[2] @'F32+                        y <- arg @'[2] @'F32+                        p <- arg @'[] @'Bool+                        z <- conditional2 p (returnTuple2 x x) (returnTuple2 y y)+                        return z+            let rendered = render modu+            assertBool "stablehlo.if" $ "stablehlo.if" `T.isInfixOf` rendered+        ]+    , testGroup "RNG"+        [ testCase "rngUniform" $ do+            let modu = moduleFromBuilder @'[2, 2] @'F32 "main" [] $ do+                    a <- constant @'[] @'F32 0.0+                    b <- constant @'[] @'F32 1.0+                    r <- rngUniform a b+                    return r+            let rendered = render modu+            assertBool "stablehlo.rng" $ "stablehlo.rng" `T.isInfixOf` rendered+            assertBool "UNIFORM" $ "UNIFORM" `T.isInfixOf` rendered+        , testCase "rngNormal" $ do+            let modu = moduleFromBuilder @'[2, 2] @'F32 "main" [] $ do+                    r <- rngNormal+                    return r+            let rendered = render modu+            assertBool "stablehlo.rng" $ "stablehlo.rng" `T.isInfixOf` rendered+            assertBool "NORMAL" $ "NORMAL" `T.isInfixOf` rendered+        , testCase "rngBitGenerator" $ do+            let modu = moduleFromBuilder2 @'[2] @'UI64 @'[4] @'UI64 "main" [] $ do+                    s <- constant @'[2] @'UI64 1.0+                    (s', r) <- rngBitGenerator s+                    returnTuple2 s' r+            let rendered = render modu+            assertBool "stablehlo.rng_bit_generator" $ "stablehlo.rng_bit_generator" `T.isInfixOf` rendered+            assertBool "THREE_FRY" $ "THREE_FRY" `T.isInfixOf` rendered+        ]     ]
test/Test/IR/Pretty.hs view
@@ -32,7 +32,7 @@                     [TensorType [2, 2] F32]                     [ValueId 2]                     [ Operation "stablehlo.add" [ValueId 0, ValueId 1]-                        [TensorType [2, 2] F32, TensorType [2, 2] F32] [] [] (ValueId 2) (TensorType [2, 2] F32)+                        [TensorType [2, 2] F32, TensorType [2, 2] F32] [] [] [ValueId 2] [TensorType [2, 2] F32]                     ]             let expected =                     "func.func @main(%arg0: tensor<2x2xf32>, %arg1: tensor<2x2xf32>) -> tensor<2x2xf32> {\n"@@ -44,7 +44,7 @@             let op = Operation "stablehlo.broadcast_in_dim" [ValueId 0]                     [TensorType [3] F32]                     [AttrIntList "broadcast_dimensions" [1]]-                    [] (ValueId 1) (TensorType [2, 3] F32)+                    [] [ValueId 1] [TensorType [2, 3] F32]             let rendered = render op             assertBool "should contain trailing dims" $                 ", dims = [1]" `T.isInfixOf` rendered@@ -52,13 +52,13 @@             let op = Operation "stablehlo.compare" [ValueId 0, ValueId 1]                     [TensorType [2] F32, TensorType [2] F32]                     [AttrString "comparison_direction" "LT"]-                    [] (ValueId 2) (TensorType [2] Bool)+                    [] [ValueId 2] [TensorType [2] Bool]             let rendered = render op             assertBool "should contain inline direction" $                 "\"LT\"" `T.isInfixOf` rendered         , testCase "return generic form" $ do             let op = Operation "stablehlo.return" [ValueId 0]-                    [TensorType [] F32] [] [] (ValueId 0) (TensorType [] F32)+                    [TensorType [] F32] [] [] [ValueId 0] [TensorType [] F32]             let rendered = render op             assertBool "should be generic form" $                 "\"stablehlo.return\"" `T.isInfixOf` rendered@@ -66,18 +66,28 @@     , testGroup "Constants"         [ testCase "scalar constant" $ do             let op = Operation "stablehlo.constant" []-                    [] [AttrDenseElements [] F32 [3.0]] [] (ValueId 0) (TensorType [] F32)+                    [] [AttrDenseElements [] F32 [3.0]] [] [ValueId 0] [TensorType [] F32]             let rendered = render op             assertBool "dense scalar" $ "dense<3.0>" `T.isInfixOf` rendered         , testCase "1D constant" $ do             let op = Operation "stablehlo.constant" []-                    [] [AttrDenseElements [3] F32 [1.0, 2.0, 3.0]] [] (ValueId 0) (TensorType [3] F32)+                    [] [AttrDenseElements [3] F32 [1.0, 2.0, 3.0]] [] [ValueId 0] [TensorType [3] F32]             let rendered = render op             assertBool "dense 1D" $ "dense<[1.0, 2.0, 3.0]>" `T.isInfixOf` rendered         , testCase "2D constant" $ do             let op = Operation "stablehlo.constant" []-                    [] [AttrDenseElements [2, 2] F32 [1.0, 2.0, 3.0, 4.0]] [] (ValueId 0) (TensorType [2, 2] F32)+                    [] [AttrDenseElements [2, 2] F32 [1.0, 2.0, 3.0, 4.0]] [] [ValueId 0] [TensorType [2, 2] F32]             let rendered = render op             assertBool "dense 2D" $ "dense<[[1.0, 2.0], [3.0, 4.0]]>" `T.isInfixOf` rendered+        ]+    , testGroup "Multi-result ops"+        [ testCase "two results" $ do+            let op = Operation "stablehlo.rng_bit_generator" [ValueId 0]+                    [TensorType [2] UI64]+                    [AttrRaw "rng_algorithm = #stablehlo<rng_algorithm THREE_FRY>"]+                    [] [ValueId 1, ValueId 2] [TensorType [2] UI64, TensorType [4] UI64]+            let rendered = render op+            assertBool "two result vids" $ "%1, %2 =" `T.isInfixOf` rendered+            assertBool "rng_bit_generator" $ "stablehlo.rng_bit_generator" `T.isInfixOf` rendered         ]     ]
+ test/Test/Runtime/EndToEndMultiValue.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Test.Runtime.EndToEndMultiValue (tests) where++import qualified Data.Vector.Storable as V+import Test.Tasty+import Test.Tasty.HUnit+import Prelude hiding (compare)++import HHLO.Core.Types+import HHLO.EDSL.Ops+import HHLO.IR.AST (FuncArg(..), TensorType(..), Module)+import HHLO.IR.Builder+import HHLO.IR.Pretty+import HHLO.Runtime.PJRT.Plugin+import HHLO.Runtime.PJRT.Types+import HHLO.Runtime.Compile+import HHLO.Runtime.Execute+import HHLO.Runtime.Buffer+import Data.Int (Int64)++-- | A loop that counts from a starting value up to a limit, accumulating a sum.+-- Inputs: (counter_init, sum_init)+-- While counter < limit:+--   counter = counter + 1+--   sum = sum + counter+-- Returns: (final_counter, final_sum)+while2Module :: Module+while2Module =+    moduleFromBuilder2 @'[] @'I64 @'[] @'I64 "main"+        [ FuncArg "arg0" (TensorType [] I64)+        , FuncArg "arg1" (TensorType [] I64)+        ]+        $ do+            counter0 <- arg @'[] @'I64+            sum0 <- arg @'[] @'I64+            result <- whileLoop2 counter0 sum0+                (\c _s -> do+                    limitC <- constant @'[] @'I64 3+                    cond <- compare c limitC "LT"+                    return cond)+                (\c s -> do+                    one <- constant @'[] @'I64 1+                    cNext <- add c one+                    sNext <- add s cNext+                    returnTuple2 cNext sNext)+            return result++cond2Module :: Module+cond2Module =+    moduleFromBuilder2 @'[] @'I64 @'[] @'I64 "main"+        [ FuncArg "arg0" (TensorType [] Bool) ]+        $ do+            p <- arg @'[] @'Bool+            t1 <- constant @'[] @'I64 1+            t2 <- constant @'[] @'I64 2+            f1 <- constant @'[] @'I64 3+            f2 <- constant @'[] @'I64 4+            result <- conditional2 p (returnTuple2 t1 t2) (returnTuple2 f1 f2)+            return result++tests :: TestTree+tests = testGroup "EndToEnd.MultiValue"+    [ testCase "whileLoop2 counts and sums" $ withPJRTCPU $ \api client -> do+        let mlirText = render while2Module+        exec <- compile api client mlirText+        bufCounter <- toDevice api client (V.fromList [0 :: Int64]) [1] bufferTypeS64+        bufSum <- toDevice api client (V.fromList [0 :: Int64]) [1] bufferTypeS64+        [bufOut1, bufOut2] <- execute api exec [bufCounter, bufSum]+        result1 <- fromDevice api bufOut1 1 :: IO (V.Vector Int64)+        result2 <- fromDevice api bufOut2 1 :: IO (V.Vector Int64)+        -- counter goes 0->1->2->3, sum goes 0->1->3->6+        result1 @?= V.fromList [3]+        result2 @?= V.fromList [6]+    , testCase "conditional2 true branch" $ withPJRTCPU $ \api client -> do+        let mlirText = render cond2Module+        exec <- compile api client mlirText+        bufPred <- toDevice api client (V.fromList [1 :: Int64]) [1] bufferTypePred+        [bufOut1, bufOut2] <- execute api exec [bufPred]+        result1 <- fromDevice api bufOut1 1 :: IO (V.Vector Int64)+        result2 <- fromDevice api bufOut2 1 :: IO (V.Vector Int64)+        result1 @?= V.fromList [1]+        result2 @?= V.fromList [2]+    , testCase "conditional2 false branch" $ withPJRTCPU $ \api client -> do+        let mlirText = render cond2Module+        exec <- compile api client mlirText+        bufPred <- toDevice api client (V.fromList [0 :: Int64]) [1] bufferTypePred+        [bufOut1, bufOut2] <- execute api exec [bufPred]+        result1 <- fromDevice api bufOut1 1 :: IO (V.Vector Int64)+        result2 <- fromDevice api bufOut2 1 :: IO (V.Vector Int64)+        result1 @?= V.fromList [3]+        result2 @?= V.fromList [4]+    ]