Skip to main content

tensor_wasm_jit/
ptx_emit.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Craton Software Company
3//! PTX text emitter.
4//!
5//! Lowers a [`TensorWasmKernelBlueprint`] to a PTX assembly text suitable for
6//! `cust::module::Module::from_ptx` (CUDA-host path) or `ptxas` (CI
7//! validation). The emitter targets sm_80 — Ampere — which is the lowest
8//! deployed architecture TensorWasm claims first-class support for.
9//!
10//! ## Register allocation
11//!
12//! Each lowered op produces a fresh f32 register (`%fN`). A `Vec<u32>`
13//! "value stack" tracks the registers currently live on the abstract TensorWasm
14//! stack — `Op::VecAdd` pops two operands and pushes one, etc. The high-
15//! water mark of allocated registers is recorded so the kernel header
16//! declares an exact-sized `.reg .f32 %f<MAX>` rather than a fixed `8`.
17//!
18//! Loads pull a fresh `%f` from the input buffer (`%rd0`) at a running
19//! byte offset that advances by 4 bytes per lane. Stores pop their operand
20//! and write it to the output buffer (`%rd1`) at the corresponding offset.
21//! This is a contract with `tensor_wasm_exec::jit_dispatch`: the scratch buffer's
22//! `args` half maps to `in_ptr`, the `results` half maps to `out_ptr`.
23//!
24//! ## What is intentionally NOT lowered (default)
25//!
26//! [`TensorWasmOp::MatMul`] is **by default** rejected at emit time with
27//! [`EmitError::NotYetImplemented`]. A real wmma lowering needs fragment-
28//! handle materialisation (loading the `a`/`b` operand tiles into the
29//! fragment register handles) and a paired store that writes the
30//! accumulator fragments back to global memory — neither of which the
31//! default IR-to-PTX path encodes. Emitting a syntactically-valid-but-
32//! semantically-broken wmma block would silently corrupt GPU state on
33//! launch, so we refuse instead. See v0.4 roadmap.
34//!
35//! ## EXPERIMENTAL / UNVERIFIED ON HARDWARE: opt-in wmma MatMul
36//!
37//! Setting [`EmitConfig::enable_experimental_matmul`] to `true` flips the
38//! emitter into lowering [`TensorWasmOp::MatMul`] with `m == n == k == 16`
39//! to a wmma `m16n16k16` fragment-load → `wmma.mma.sync` → fragment-store
40//! sequence. This flag defaults to `false` and the safe refusal above
41//! remains the default behaviour; nothing in the auto-offload pipeline
42//! sets it. The generated PTX is **NOT verified on real GPU hardware** in
43//! this environment — its structural correctness (fragment shapes, mma.sync
44//! count, accumulator chaining, register declarations) is gated behind the
45//! differential oracle in `crate::differential`, which MUST pass before
46//! anyone enables this flag in a shipping path.
47//!
48//! ### wmma m16n16k16 tile / fragment contract
49//!
50//! The lowering targets the sm_80 `wmma.mma.sync.aligned.row.col.m16n16k16`
51//! shape with f16 operand fragments and an f32 accumulator (`.f32.f32`):
52//!
53//! * **Tile shape.** One warp cooperatively computes a single
54//!   `C[16x16] += A[16x16] * B[16x16]` tile. A is loaded `row` major, B is
55//!   loaded `col` major (the canonical `.row.col` operand layout).
56//! * **Operand fragments.** Each of the A and B operand fragments is held
57//!   in [`WMMA_OPERAND_FRAG_REGS`] (`8`) packed `.b32` registers per warp
58//!   (two f16 lanes per `.b32`), materialised with
59//!   `wmma.load.a`/`wmma.load.b`.
60//! * **Accumulator fragment.** The C accumulator is held in
61//!   [`WMMA_ACC_FRAG_REGS`] (`8`) `.f32` registers per warp, initialised
62//!   from global memory with `wmma.load.c` and written back with
63//!   `wmma.store.d`. The accumulator is *chained*: the `wmma.mma.sync`
64//!   reads the same `%fa…` accumulator registers it writes (paired
65//!   accumulator — `D = A*B + C` with `C` and `D` aliasing the same
66//!   fragment), so back-to-back tiles over a longer `k` would accumulate
67//!   correctly.
68//! * **Alignment / leading dimension.** `wmma.load.*`/`wmma.store.*`
69//!   require the base pointer to be **128-bit aligned** and the supplied
70//!   leading-dimension (stride, in elements) to be a multiple of 8 for the
71//!   f16 operands and a multiple of 4 for the f32 accumulator. We assume a
72//!   tightly-packed row-major tile, so the stride equals the tile width
73//!   (`16`) and the caller guarantees the `in_ptr`/`out_ptr` buffers are
74//!   16-byte aligned (the unified-memory allocator already over-aligns to
75//!   256 bytes, so this holds for every legitimate caller).
76
77use std::fmt::Write;
78
79use thiserror::Error;
80
81use crate::ir::{ElemType, TensorWasmKernelBlueprint, TensorWasmOp};
82
83/// PTX register class a SIMD lane is materialised in.
84///
85/// jit CRITICAL fix: integer SIMD lanes go in the `.s32 %r` class and use
86/// typed integer instructions; float lanes stay in the `.f32 %f` class.
87/// Before this, every lane used `%f` + `.f32` ops regardless of element
88/// type, silently miscompiling integer SIMD.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90enum RegClass {
91    /// `.f32 %f` registers.
92    F32,
93    /// `.s32 %r` registers (32-bit integers).
94    S32,
95}
96
97impl RegClass {
98    /// The register class for a given element type. Only the 4-byte lane
99    /// widths the emitter supports end-to-end reach here (`f32`, `i32`);
100    /// other widths are failed closed upstream in
101    /// `rewrite::op_to_detector_op`, so a wider type defaulting to `S32`
102    /// here is never exercised by the production path.
103    fn for_elem(elem: ElemType) -> Self {
104        if elem.is_float() {
105            RegClass::F32
106        } else {
107            RegClass::S32
108        }
109    }
110
111    /// The `%`-prefix character for this class (`f` or `r`).
112    fn prefix(self) -> char {
113        match self {
114            RegClass::F32 => 'f',
115            RegClass::S32 => 'r',
116        }
117    }
118}
119
120/// PTX `add` mnemonic for an element type.
121///
122/// Integer add is sign-agnostic in two's complement, so `add.s32` is
123/// correct for both signed and unsigned i32 lanes. Only the widths the
124/// emitter supports end-to-end are accepted; anything else refuses so the
125/// caller deopts rather than emitting a wrong-width instruction.
126fn add_mnemonic(elem: ElemType) -> Result<&'static str, EmitError> {
127    match elem {
128        ElemType::F32 => Ok("add.f32"),
129        ElemType::I32 => Ok("add.s32"),
130        _ => Err(EmitError::NotYetImplemented(
131            "PTX add for this SIMD element width is not yet emittable",
132        )),
133    }
134}
135
136/// PTX `mul` mnemonic for an element type. Integer multiply takes the low
137/// half (`mul.lo.s32`) — the same lane width in, lane width out, matching
138/// WASM `i32x4.mul` semantics.
139fn mul_mnemonic(elem: ElemType) -> Result<&'static str, EmitError> {
140    match elem {
141        ElemType::F32 => Ok("mul.f32"),
142        ElemType::I32 => Ok("mul.lo.s32"),
143        _ => Err(EmitError::NotYetImplemented(
144            "PTX mul for this SIMD element width is not yet emittable",
145        )),
146    }
147}
148
149/// PTX fused-multiply-add mnemonic. Float only (single rounding).
150fn fma_mnemonic(elem: ElemType) -> Result<&'static str, EmitError> {
151    match elem {
152        ElemType::F32 => Ok("fma.rn.f32"),
153        _ => Err(EmitError::NotYetImplemented(
154            "PTX fma for this SIMD element width is not yet emittable",
155        )),
156    }
157}
158
159/// PTX load/store type suffix for an element type. Integer loads/stores use
160/// the bit-width `.u32` form (the bit pattern is type-agnostic at the
161/// memory boundary); floats use `.f32`.
162fn load_store_type(elem: ElemType) -> &'static str {
163    if elem.is_float() {
164        "f32"
165    } else {
166        "u32"
167    }
168}
169
170/// Errors produced by the PTX emitter.
171#[derive(Debug, Clone, Error, PartialEq, Eq)]
172pub enum EmitError {
173    /// The blueprint contains an op the emitter does not yet lower to PTX.
174    /// Callers should treat this as "keep this function on the CPU path".
175    #[error("PTX emission not yet implemented: {0}")]
176    NotYetImplemented(&'static str),
177    /// The blueprint's `entry` field is not a valid PTX identifier.
178    ///
179    /// Today every legitimate caller constructs `entry` via
180    /// `format!("func{idx}")` so this can never fire — but the Pliron
181    /// `UserFuncName::Testcase` path can carry tenant-controlled bytes that
182    /// would otherwise be interpolated unescaped into the PTX template
183    /// (closing the kernel scope, emitting a second adversary kernel, etc.).
184    /// Closes jit S-1.
185    #[error("invalid PTX entry-name: {entry}")]
186    InvalidEntryName {
187        /// The rejected identifier (echoed for diagnostic purposes only —
188        /// callers MUST NOT include the value in untrusted-facing errors).
189        entry: String,
190    },
191    /// The blueprint demands more SSA registers than the `%fN` index space
192    /// (`u32`) can address. Previously the allocator saturated at
193    /// `u32::MAX`, which silently aliased two distinct SSA values onto the
194    /// same physical register and corrupted results. We now refuse the
195    /// blueprint so the caller (rewrite.rs) deopts to the CPU path rather
196    /// than launch a miscompiled kernel. Closes jit L1.
197    #[error("PTX register allocation overflowed the u32 index space")]
198    TooManyRegisters,
199    /// The blueprint's emission budget (per-op `lanes`, or total ops *
200    /// lanes across the stream) exceeds the emitter's DoS guardrails.
201    ///
202    /// The lane-bearing ops (`VecAdd`/`VecMul`/`VecFma`/`LoadUnified`/
203    /// `StoreUnified`) each drive a `0..lanes` emission loop. The
204    /// production auto-offload path pins `lanes` to `DEFAULT_LANES = 4`,
205    /// but the PUBLIC `TensorWasmKernelBlueprint::push` + `emit`/`emit_with`
206    /// API accepts arbitrary `lanes` (up to `u32::MAX`). An embedder
207    /// building blueprints from untrusted dimensions could request billions
208    /// of PTX lines and exhaust CPU/memory before a byte of output is
209    /// returned. We bound `lanes` by [`MAX_LANES`] and the aggregate
210    /// (ops * lanes) emission budget by [`MAX_EMITTED_OPS`] and refuse up
211    /// front rather than rely on every caller to pre-bound. Closes jit L2
212    /// (unbounded `lanes` / op-count emission DoS).
213    #[error("PTX emission budget exceeded: {what}")]
214    EmissionBudgetExceeded {
215        /// Which limit was hit (for diagnostics only — this string is
216        /// static and carries no untrusted-derived value).
217        what: &'static str,
218    },
219}
220
221/// Maximum length of a PTX identifier (NVIDIA PTX ISA §A.3).
222pub const MAX_PTX_IDENTIFIER_LEN: usize = 1024;
223
224/// Returns `true` iff `s` matches `[A-Za-z_][A-Za-z0-9_$]*` and is no
225/// longer than [`MAX_PTX_IDENTIFIER_LEN`] bytes.
226///
227/// This is the validator that closes jit S-1 (PTX injection via guest-
228/// controlled entry name). Every value that ends up interpolated into
229/// the `.visible .entry {entry}(` template MUST be screened through this
230/// function first.
231#[must_use]
232pub fn is_valid_ptx_identifier(s: &str) -> bool {
233    if s.is_empty() || s.len() > MAX_PTX_IDENTIFIER_LEN {
234        return false;
235    }
236    let mut bytes = s.bytes();
237    let first = bytes.next().unwrap();
238    if !(first.is_ascii_alphabetic() || first == b'_') {
239        return false;
240    }
241    bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'$')
242}
243
244/// Number of `.b32` registers in one wmma `m16n16k16` operand (A or B)
245/// fragment, per warp. f16 operands pack two lanes per `.b32`, so a
246/// 16-wide row spread across the warp's 32 lanes lands in 8 registers.
247pub const WMMA_OPERAND_FRAG_REGS: u32 = 8;
248
249/// Number of `.f32` registers in the wmma `m16n16k16` f32 accumulator
250/// fragment, per warp. The 16x16 = 256 accumulator elements distributed
251/// over 32 lanes give 8 elements (registers) per lane.
252pub const WMMA_ACC_FRAG_REGS: u32 = 8;
253
254/// The only MatMul tile dimension the experimental wmma lowering accepts.
255/// `m == n == k == 16` is the single sm_80 wmma shape this emitter models;
256/// any other dimensions fall back to [`EmitError::NotYetImplemented`] even
257/// when the experimental flag is on.
258pub const WMMA_TILE_DIM: u32 = 16;
259
260/// DoS guardrail (jit L2): maximum `lanes` any single lane-bearing op may
261/// request. Each lane-bearing op (`VecAdd`/`VecMul`/`VecFma`/`LoadUnified`/
262/// `StoreUnified`) emits one PTX instruction line per lane in a `0..lanes`
263/// loop, so an attacker-supplied `lanes` near `u32::MAX` would attempt to
264/// emit billions of lines. `1024` is a generous upper bound that still
265/// comfortably covers realistic GPU lane / vector widths (the production
266/// auto-offload path only ever uses `DEFAULT_LANES = 4`).
267pub const MAX_LANES: u32 = 1024;
268
269/// DoS guardrail (jit L2): maximum aggregate emitted-op budget, i.e. the
270/// sum of `lanes` over every lane-bearing op in the blueprint (each lane
271/// emits ~one instruction line). Bounds total emission work even when each
272/// individual op is under [`MAX_LANES`] but the op stream is long. `1 << 20`
273/// (~1M instruction lines, low tens of MiB of PTX text) is well beyond any
274/// realistic kernel yet small enough to reject adversarial blueprints
275/// before any output bytes are produced.
276pub const MAX_EMITTED_OPS: u64 = 1 << 20;
277
278/// Default PTX target architecture.
279pub const DEFAULT_TARGET: &str = "sm_80";
280
281/// Default PTX language version.
282pub const DEFAULT_PTX_VERSION: &str = "8.0";
283
284/// Configuration knobs for the emitter.
285#[derive(Debug, Clone)]
286pub struct EmitConfig {
287    /// PTX `.target` directive value (e.g. "sm_80", "sm_89").
288    pub target: String,
289    /// PTX `.version` directive value (e.g. "8.0").
290    pub ptx_version: String,
291    /// Emit `__launch_bounds__` annotation on the entry.
292    pub launch_bounds: bool,
293    /// EXPERIMENTAL / UNVERIFIED ON HARDWARE.
294    ///
295    /// When `true`, [`emit_with`] lowers a `MatMul { m: 16, n: 16, k: 16 }`
296    /// op to a wmma `m16n16k16` fragment-load → `wmma.mma.sync` →
297    /// fragment-store sequence (see the module docs for the tile/fragment
298    /// contract). When `false` — the default — `MatMul` is refused with
299    /// [`EmitError::NotYetImplemented`], the safe behaviour the
300    /// auto-offload pipeline relies on.
301    ///
302    /// This MUST stay `false` outside of explicitly opted-in experiments:
303    /// the emitted PTX cannot be validated on a GPU here, so its
304    /// correctness is gated solely behind the differential oracle's
305    /// structural assertions (see `crate::differential`).
306    pub enable_experimental_matmul: bool,
307}
308
309impl Default for EmitConfig {
310    fn default() -> Self {
311        Self {
312            target: DEFAULT_TARGET.to_string(),
313            ptx_version: DEFAULT_PTX_VERSION.to_string(),
314            launch_bounds: true,
315            // SAFETY DEFAULT: MatMul stays refused unless a caller flips
316            // this on for an experiment. Never default this to `true`.
317            enable_experimental_matmul: false,
318        }
319    }
320}
321
322/// Result of emitting a blueprint.
323#[derive(Debug, Clone)]
324pub struct EmittedPtx {
325    /// The PTX text.
326    pub text: String,
327    /// Launch geometry the blueprint expects.
328    pub launch_geometry: (u32, u32),
329}
330
331impl EmittedPtx {
332    /// Byte length of the PTX text.
333    pub fn len(&self) -> usize {
334        self.text.len()
335    }
336
337    /// True if the PTX text is empty.
338    pub fn is_empty(&self) -> bool {
339        self.text.is_empty()
340    }
341}
342
343/// Emit PTX text for a blueprint with default config.
344///
345/// Returns [`EmitError::NotYetImplemented`] for blueprints containing ops
346/// the emitter cannot lower (currently: [`TensorWasmOp::MatMul`]).
347pub fn emit(blueprint: &TensorWasmKernelBlueprint) -> Result<EmittedPtx, EmitError> {
348    emit_with(blueprint, &EmitConfig::default())
349}
350
351/// Allocate fresh registers and track the per-op body lines.
352///
353/// Returns `(body_text, max_f32_reg, max_s32_reg, max_s64_reg,
354/// max_pred_reg, max_b32_reg)` where `max_*` is one greater than the
355/// highest register index used (i.e. the count to declare in
356/// `.reg .f32 %f<N>;`). Counts are floored at 1 so the declaration is
357/// always well-formed even for empty kernels. `max_b32_reg` is the wmma
358/// operand-fragment `.b32 %rb<N>` count and is `0` unless an experimental
359/// MatMul was lowered.
360fn lower_body(
361    blueprint: &TensorWasmKernelBlueprint,
362    cfg: &EmitConfig,
363) -> Result<(String, u32, u32, u32, u32, u32), EmitError> {
364    // PERF (T20): pre-size the body buffer rather than letting `writeln!`
365    // grow it by doubling. Each lowered op emits a comment line plus one
366    // or more instruction lines (`add.f32 %f… %f… %f…;`, ~32 ASCII chars
367    // each); 80 chars per op is a comfortable upper-bound average across
368    // VecAdd/VecMul/VecFma/Load/Store lowering, so this single allocation
369    // covers the common case without a single realloc. Empty blueprints
370    // still get a usable (zero-byte) capacity.
371    let mut body = String::with_capacity(blueprint.ops.len() * 80);
372    // Pre-size the value stack to `ops.len()` — each lowered op pushes at
373    // most one register, and most pop more than they push, so this is a
374    // safe upper bound that avoids grow-by-doubling reallocs on the hot
375    // emit path. The stack holds register *indices*; the register class is
376    // implicit per block — `clif_lower::infer_block_shape` guarantees a
377    // block is element-type-homogeneous, so every stacked register belongs
378    // to that block's single class (`%f` for float, `%r` for integer).
379    let mut value_stack: Vec<u32> = Vec::with_capacity(blueprint.ops.len());
380    let mut next_f = 0u32;
381    // Integer compute register counter. Starts at 1: `%r0` is reserved for
382    // the element count `n` loaded in the prologue. The high-water mark
383    // becomes the `.reg .s32 %r<N>` declaration count.
384    //
385    // jit CRITICAL fix: previously the s32 class was fixed at 1 (only `%r0`)
386    // because every op — integer SIMD included — was emitted with `.f32`
387    // ops into the `%f` class, silently miscompiling integer SIMD. Integer
388    // lanes now allocate real `%r` registers and use typed integer ops.
389    let mut next_r = 1u32;
390    let mut max_s64 = 2u32; // %rd0 (in_ptr), %rd1 (out_ptr)
391    let max_pred = 2u32;
392    // EXPERIMENTAL: high-water mark of the `.b32` wmma operand-fragment
393    // register class (`%rb<N>`). Stays 0 unless an experimental MatMul is
394    // lowered, in which case the header declares exactly the fragment
395    // registers the `wmma.load.a`/`wmma.load.b` ops use.
396    let mut max_b32 = 0u32;
397    // Running byte offset into the input / output buffers; advances by the
398    // element width per lane (4 B for f32/i32, 8 B for f64/i64, …). Loads
399    // consume from `%rd0+in_off`; stores consume from `%rd1+out_off`.
400    let mut in_off: u32 = 0;
401    let mut out_off: u32 = 0;
402
403    // Allocate a fresh register in the given class. Returns
404    // `EmitError::TooManyRegisters` rather than saturating at `u32::MAX` —
405    // saturation would alias two distinct SSA values onto the same physical
406    // register (jit L1).
407    let alloc = |class: RegClass, next_f: &mut u32, next_r: &mut u32| -> Result<u32, EmitError> {
408        let counter = match class {
409            RegClass::F32 => next_f,
410            RegClass::S32 => next_r,
411        };
412        let r = *counter;
413        *counter = counter.checked_add(1).ok_or(EmitError::TooManyRegisters)?;
414        Ok(r)
415    };
416    // Pop an operand register off the value stack, or allocate a fresh one
417    // in `class` if the stack underflows (an op consuming more than the
418    // abstract stack holds). Threads the fallible allocator through the
419    // underflow path.
420    let pop_or_alloc = |value_stack: &mut Vec<u32>,
421                        class: RegClass,
422                        next_f: &mut u32,
423                        next_r: &mut u32|
424     -> Result<u32, EmitError> {
425        match value_stack.pop() {
426            Some(idx) => Ok(idx),
427            None => alloc(class, next_f, next_r),
428        }
429    };
430
431    for op in &blueprint.ops {
432        match op {
433            TensorWasmOp::VecAdd { elem, lanes } => {
434                let class = RegClass::for_elem(*elem);
435                let _ = writeln!(body, "    // vec_add.{elem}[{lanes}] lanes");
436                let mnemonic = add_mnemonic(*elem)?;
437                let p = class.prefix();
438                for _ in 0..*lanes {
439                    let b = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
440                    let a = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
441                    let dst = alloc(class, &mut next_f, &mut next_r)?;
442                    let _ = writeln!(body, "    {mnemonic} %{p}{dst}, %{p}{a}, %{p}{b};");
443                    value_stack.push(dst);
444                }
445            }
446            TensorWasmOp::VecMul { elem, lanes } => {
447                let class = RegClass::for_elem(*elem);
448                let _ = writeln!(body, "    // vec_mul.{elem}[{lanes}] lanes");
449                let mnemonic = mul_mnemonic(*elem)?;
450                let p = class.prefix();
451                for _ in 0..*lanes {
452                    let b = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
453                    let a = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
454                    let dst = alloc(class, &mut next_f, &mut next_r)?;
455                    let _ = writeln!(body, "    {mnemonic} %{p}{dst}, %{p}{a}, %{p}{b};");
456                    value_stack.push(dst);
457                }
458            }
459            TensorWasmOp::VecFma { elem, lanes } => {
460                // FMA is a single-rounding float primitive. Integer "fma"
461                // has no single-rounding semantics and the detector never
462                // produces an integer `VecFma`, but refuse defensively
463                // rather than emit a wrong instruction.
464                if !elem.is_float() {
465                    return Err(EmitError::NotYetImplemented(
466                        "integer VecFma has no single-rounding PTX form",
467                    ));
468                }
469                let class = RegClass::for_elem(*elem);
470                let _ = writeln!(body, "    // vec_fma.{elem}[{lanes}] lanes");
471                let mnemonic = fma_mnemonic(*elem)?;
472                let p = class.prefix();
473                for _ in 0..*lanes {
474                    let c = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
475                    let b = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
476                    let a = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
477                    let dst = alloc(class, &mut next_f, &mut next_r)?;
478                    let _ = writeln!(body, "    {mnemonic} %{p}{dst}, %{p}{a}, %{p}{b}, %{p}{c};");
479                    value_stack.push(dst);
480                }
481            }
482            TensorWasmOp::MatMul { m, n, k } => {
483                // SAFETY DEFAULT: unless the caller explicitly opted into
484                // the experimental wmma lowering, MatMul is refused so the
485                // auto-offload pipeline deopts to the CPU path rather than
486                // launch a kernel that cannot be verified on hardware here.
487                if !cfg.enable_experimental_matmul {
488                    return Err(EmitError::NotYetImplemented(
489                        "MatMul lowering deferred to v0.4",
490                    ));
491                }
492                // The experimental lowering models exactly one sm_80 wmma
493                // shape: m16n16k16. Any other tile dimension is still
494                // refused even with the flag on — emitting an unmodelled
495                // shape would be the same silent-miscompile hazard the
496                // refusal exists to prevent.
497                if (*m, *n, *k) != (WMMA_TILE_DIM, WMMA_TILE_DIM, WMMA_TILE_DIM) {
498                    return Err(EmitError::NotYetImplemented(
499                        "experimental wmma lowering only models m16n16k16",
500                    ));
501                }
502                // EXPERIMENTAL / UNVERIFIED ON HARDWARE.
503                //
504                // wmma m16n16k16 fragment-load → wmma.mma.sync →
505                // fragment-store. See the module docs for the full
506                // tile/fragment + alignment contract. The PTX produced
507                // here is validated ONLY by the differential oracle's
508                // structural assertions; it has NOT been run on a GPU.
509                lower_wmma_m16n16k16(&mut body, &mut next_f, &mut max_b32, &mut max_s64)?;
510            }
511            TensorWasmOp::LoadUnified { elem, lanes } => {
512                let class = RegClass::for_elem(*elem);
513                let p = class.prefix();
514                let ld_ty = load_store_type(*elem);
515                let width = elem.byte_width();
516                let _ = writeln!(
517                    body,
518                    "    // load_unified.{elem}[{lanes}] lanes (.lu cache hint) from %rd0+{in_off}"
519                );
520                for _ in 0..*lanes {
521                    let dst = alloc(class, &mut next_f, &mut next_r)?;
522                    if in_off == 0 {
523                        let _ = writeln!(body, "    ld.global.lu.{ld_ty} %{p}{dst}, [%rd0];");
524                    } else {
525                        let _ =
526                            writeln!(body, "    ld.global.lu.{ld_ty} %{p}{dst}, [%rd0+{in_off}];");
527                    }
528                    // jit CRITICAL fix: advance by the element width, not a
529                    // hard-coded 4. `checked_add` so a pathological lane
530                    // count refuses rather than wrapping the offset and
531                    // aliasing two lanes onto the same byte.
532                    in_off = in_off
533                        .checked_add(width)
534                        .ok_or(EmitError::TooManyRegisters)?;
535                    value_stack.push(dst);
536                }
537            }
538            TensorWasmOp::StoreUnified { elem, lanes } => {
539                let class = RegClass::for_elem(*elem);
540                let p = class.prefix();
541                let ld_ty = load_store_type(*elem);
542                let width = elem.byte_width();
543                let _ = writeln!(
544                    body,
545                    "    // store_unified.{elem}[{lanes}] lanes (.cs cache hint) to %rd1+{out_off}"
546                );
547                for _ in 0..*lanes {
548                    let src = pop_or_alloc(&mut value_stack, class, &mut next_f, &mut next_r)?;
549                    if out_off == 0 {
550                        let _ = writeln!(body, "    st.global.cs.{ld_ty} [%rd1], %{p}{src};");
551                    } else {
552                        let _ = writeln!(
553                            body,
554                            "    st.global.cs.{ld_ty} [%rd1+{out_off}], %{p}{src};"
555                        );
556                    }
557                    out_off = out_off
558                        .checked_add(width)
559                        .ok_or(EmitError::TooManyRegisters)?;
560                }
561            }
562            TensorWasmOp::Barrier => {
563                let _ = writeln!(body, "    bar.sync 0;");
564            }
565        }
566    }
567
568    // `.reg .* %x<N>;` requires N >= 1, even if no register is used.
569    let max_f = next_f.max(1);
570    let max_s32 = next_r.max(1);
571    Ok((body, max_f, max_s32, max_s64, max_pred, max_b32))
572}
573
574/// EXPERIMENTAL / UNVERIFIED ON HARDWARE.
575///
576/// Lower one `MatMul { m: 16, n: 16, k: 16 }` op to a wmma `m16n16k16`
577/// fragment-load → `wmma.mma.sync` → fragment-store sequence targeting
578/// sm_80 (`.row.col`, f16 operands, f32 accumulator).
579///
580/// The emitted sequence is, per warp:
581///
582/// 1. Materialise the A operand fragment ([`WMMA_OPERAND_FRAG_REGS`]
583///    `.b32` regs) from `%rd0` with `wmma.load.a.sync.aligned.row`.
584/// 2. Materialise the B operand fragment (same width) from the second
585///    tile in `%rd0` with `wmma.load.b.sync.aligned.col`.
586/// 3. Load the C accumulator fragment ([`WMMA_ACC_FRAG_REGS`] `.f32`
587///    regs) from `%rd0`'s third tile with `wmma.load.c.sync.aligned.row`.
588/// 4. `wmma.mma.sync.aligned.row.col.m16n16k16.f32.f32` — the accumulator
589///    is **paired**: the same `%f` registers appear as both the `c`
590///    operand and the `d` result, so `D = A*B + C` chains in place.
591/// 5. Write the accumulator back to `%rd1` with
592///    `wmma.store.d.sync.aligned.row`.
593///
594/// Register accounting:
595/// * A and B fragments are allocated in the `.b32 %rb` class
596///   (`max_b32` high-water mark) — `2 * WMMA_OPERAND_FRAG_REGS` registers.
597/// * The accumulator is allocated in the existing `.f32 %f` class
598///   (`next_f`) — [`WMMA_ACC_FRAG_REGS`] registers.
599/// * The load/store base pointers are derived into the `.s64 %rd` class;
600///   the highest `%rd` index used is recorded in `max_s64`.
601///
602/// Alignment / leading-dimension assumptions are documented on the module
603/// (`row` stride == tile width == 16, 128-bit aligned base pointers).
604fn lower_wmma_m16n16k16(
605    body: &mut String,
606    next_f: &mut u32,
607    max_b32: &mut u32,
608    max_s64: &mut u32,
609) -> Result<(), EmitError> {
610    // Allocate the contiguous operand-fragment register windows. Each
611    // operand fragment occupies `WMMA_OPERAND_FRAG_REGS` `.b32` regs;
612    // `wmma.load`/`mma.sync` name the window by its base index `%rbN`.
613    let a_frag_base = *max_b32;
614    *max_b32 = max_b32
615        .checked_add(WMMA_OPERAND_FRAG_REGS)
616        .ok_or(EmitError::TooManyRegisters)?;
617    let b_frag_base = *max_b32;
618    *max_b32 = max_b32
619        .checked_add(WMMA_OPERAND_FRAG_REGS)
620        .ok_or(EmitError::TooManyRegisters)?;
621
622    // Accumulator fragment in the f32 class, chained through mma.sync.
623    let acc_frag_base = *next_f;
624    *next_f = next_f
625        .checked_add(WMMA_ACC_FRAG_REGS)
626        .ok_or(EmitError::TooManyRegisters)?;
627
628    // Derive the three tile base pointers into the s64 class. `%rd0`
629    // holds the input pointer (A tile); B and C tiles follow it at the
630    // tile-stride offset (16x16 f16 = 512 B for A/B, 16x16 f32 = 1024 B
631    // for C). `%rd1` holds the output (D tile) pointer. We materialise
632    // %rd2 (B base) and %rd3 (C base) so the loads name distinct bases.
633    let b_base = *max_s64; // %rd2
634    let c_base = b_base + 1; // %rd3
635    *max_s64 = c_base.checked_add(1).ok_or(EmitError::TooManyRegisters)?; // declare through %rd3
636
637    // Leading dimension (stride in elements) for the row/col loads. A
638    // tightly-packed 16-wide tile has stride 16.
639    let stride = WMMA_TILE_DIM;
640    // Byte offsets of the B and C tiles within the input buffer. A is
641    // 16x16 f16 (512 B); B is 16x16 f16 (512 B); C is 16x16 f32 (1024 B).
642    const A_TILE_BYTES: u32 = WMMA_TILE_DIM * WMMA_TILE_DIM * 2; // f16
643    const B_TILE_BYTES: u32 = WMMA_TILE_DIM * WMMA_TILE_DIM * 2; // f16
644    let b_tile_off = A_TILE_BYTES;
645    let c_tile_off = A_TILE_BYTES + B_TILE_BYTES;
646
647    let _ = writeln!(
648        body,
649        "    // EXPERIMENTAL / UNVERIFIED ON HARDWARE: wmma m16n16k16 \
650         (row.col, f16xf16->f32)"
651    );
652
653    // Render a `{%rbB, %rbB+1, …}` fragment register list.
654    let frag_list = |base: u32, count: u32, prefix: &str| -> String {
655        let regs: Vec<String> = (0..count)
656            .map(|i| format!("%{prefix}{}", base + i))
657            .collect();
658        format!("{{{}}}", regs.join(", "))
659    };
660    let a_list = frag_list(a_frag_base, WMMA_OPERAND_FRAG_REGS, "rb");
661    let b_list = frag_list(b_frag_base, WMMA_OPERAND_FRAG_REGS, "rb");
662    let acc_list = frag_list(acc_frag_base, WMMA_ACC_FRAG_REGS, "f");
663
664    // Tile base-pointer setup.
665    let _ = writeln!(body, "    add.s64 %rd{b_base}, %rd0, {b_tile_off};");
666    let _ = writeln!(body, "    add.s64 %rd{c_base}, %rd0, {c_tile_off};");
667
668    // 1+2. Operand-fragment loads (A row-major, B col-major).
669    let _ = writeln!(
670        body,
671        "    wmma.load.a.sync.aligned.row.m16n16k16.global.f16 {a_list}, [%rd0], {stride};"
672    );
673    let _ = writeln!(
674        body,
675        "    wmma.load.b.sync.aligned.col.m16n16k16.global.f16 {b_list}, [%rd{b_base}], {stride};"
676    );
677    // 3. Accumulator load (paired: read C into the same regs mma writes).
678    let _ = writeln!(
679        body,
680        "    wmma.load.c.sync.aligned.row.m16n16k16.global.f32 {acc_list}, [%rd{c_base}], {stride};"
681    );
682    // 4. The mma. `d` and `c` alias `acc_list` — paired accumulator.
683    let _ = writeln!(
684        body,
685        "    wmma.mma.sync.aligned.row.col.m16n16k16.f32.f32 \
686         {acc_list}, {a_list}, {b_list}, {acc_list};"
687    );
688    // 5. Store the accumulator fragment back to the D tile in %rd1.
689    let _ = writeln!(
690        body,
691        "    wmma.store.d.sync.aligned.row.m16n16k16.global.f32 [%rd1], {acc_list}, {stride};"
692    );
693
694    Ok(())
695}
696
697/// Emit PTX text for a blueprint with caller-supplied config.
698///
699/// Returns [`EmitError::NotYetImplemented`] for blueprints containing ops
700/// the emitter cannot lower (currently: [`TensorWasmOp::MatMul`]).
701pub fn emit_with(
702    blueprint: &TensorWasmKernelBlueprint,
703    cfg: &EmitConfig,
704) -> Result<EmittedPtx, EmitError> {
705    // SECURITY (jit S-1): every `blueprint.entry` value reaches the PTX
706    // template unescaped through several `writeln!` sites below
707    // (`.visible .entry {entry}(`, `{entry}_param_in_ptr`, etc.). A
708    // tenant-controlled entry like `myfunc) … \n.entry attacker(...)`
709    // would close the kernel scope and emit a second kernel. Reject
710    // anything that isn't a well-formed PTX identifier BEFORE writing
711    // a byte of output.
712    if !is_valid_ptx_identifier(&blueprint.entry) {
713        return Err(EmitError::InvalidEntryName {
714            entry: blueprint.entry.clone(),
715        });
716    }
717    // SECURITY (jit L2): bound the emission work BEFORE producing any output.
718    // The lane-bearing ops each drive a `0..lanes` loop in `lower_body`
719    // (see the emission loops in that fn), so a public-API caller supplying
720    // an unbounded `lanes` (e.g. `u32::MAX`) from untrusted dimensions would
721    // attempt to emit billions of PTX lines → CPU/memory DoS. The production
722    // auto-offload path pins `lanes` to `DEFAULT_LANES = 4`; this guard
723    // protects every other embedder of the public `push`/`emit_with` API.
724    // We reject (a) any single op whose `lanes` exceeds `MAX_LANES`, and
725    // (b) blueprints whose aggregate `ops * lanes` budget exceeds
726    // `MAX_EMITTED_OPS`. Computed in `u64` so the running sum cannot itself
727    // overflow before the cap fires.
728    let mut emitted_budget: u64 = 0;
729    for op in &blueprint.ops {
730        let lanes = match op {
731            TensorWasmOp::VecAdd { lanes, .. }
732            | TensorWasmOp::VecMul { lanes, .. }
733            | TensorWasmOp::VecFma { lanes, .. }
734            | TensorWasmOp::LoadUnified { lanes, .. }
735            | TensorWasmOp::StoreUnified { lanes, .. } => *lanes,
736            // MatMul / Barrier emit a fixed, bounded number of lines and
737            // carry no attacker-scalable `lanes` field.
738            TensorWasmOp::MatMul { .. } | TensorWasmOp::Barrier => 0,
739        };
740        if lanes > MAX_LANES {
741            return Err(EmitError::EmissionBudgetExceeded {
742                what: "single op `lanes` exceeds MAX_LANES",
743            });
744        }
745        emitted_budget = emitted_budget.saturating_add(u64::from(lanes));
746        if emitted_budget > MAX_EMITTED_OPS {
747            return Err(EmitError::EmissionBudgetExceeded {
748                what: "aggregate ops * lanes exceeds MAX_EMITTED_OPS",
749            });
750        }
751    }
752    // Rough estimate: ~64 B/op covers the per-op body line plus a fair share
753    // of the fixed prologue/epilogue. Avoids grow-by-doubling reallocs on
754    // the hot emit path.
755    let mut text = String::with_capacity(blueprint.ops.len().saturating_mul(64));
756    let _ = writeln!(text, "//");
757    let _ = writeln!(text, "// Auto-emitted by tensor-wasm-jit::ptx_emit");
758    let _ = writeln!(text, "// entry: {}", blueprint.entry);
759    let _ = writeln!(text, "// ops:   {}", blueprint.ops.len());
760    let _ = writeln!(text, "//");
761    let _ = writeln!(text);
762    let _ = writeln!(text, ".version {}", cfg.ptx_version);
763    let _ = writeln!(text, ".target {}", cfg.target);
764    let _ = writeln!(text, ".address_size 64");
765    let _ = writeln!(text);
766
767    let (grid_size, block_size) = blueprint.grid_hint.launch_geometry();
768
769    // Lower the body first so we know how many registers to declare. This
770    // is the critical fix for the prior sham allocator: declare exactly
771    // what we use, not a fixed `8` that overflowed silently.
772    let (body, max_f, max_s32, max_s64, max_pred, max_b32) = lower_body(blueprint, cfg)?;
773
774    let _ = writeln!(text, ".visible .entry {}(", blueprint.entry);
775    let _ = writeln!(text, "    .param .u64 {}_param_in_ptr,", blueprint.entry);
776    let _ = writeln!(text, "    .param .u64 {}_param_out_ptr,", blueprint.entry);
777    let _ = writeln!(text, "    .param .u32 {}_param_n", blueprint.entry);
778    let _ = writeln!(text, ")");
779    if cfg.launch_bounds {
780        // PTX directive — block size cap is hinted to ptxas via .maxntid.
781        // The runtime grid_size lives on the host side (see launch_geometry).
782        let _ = writeln!(text, ".maxntid {block_size}, 1, 1");
783    }
784    let _ = writeln!(text, "{{");
785
786    // Exact-sized register declarations.
787    let _ = writeln!(text, "    .reg .pred  %p<{max_pred}>;");
788    let _ = writeln!(text, "    .reg .s32   %r<{max_s32}>;");
789    let _ = writeln!(text, "    .reg .s64   %rd<{max_s64}>;");
790    let _ = writeln!(text, "    .reg .f32   %f<{max_f}>;");
791    // EXPERIMENTAL: wmma operand-fragment registers. Only declared when a
792    // wmma MatMul was lowered (max_b32 > 0); otherwise omitted entirely so
793    // the default-path PTX is byte-for-byte unchanged.
794    if max_b32 > 0 {
795        let _ = writeln!(text, "    .reg .b32   %rb<{max_b32}>;");
796    }
797    let _ = writeln!(text);
798
799    // Prologue: load the .param declarations into the registers the body
800    // uses. `%rd0` holds the input pointer, `%rd1` holds the output
801    // pointer, `%r0` holds the element count `n`. These mirror the host-
802    // side dispatch ABI in `tensor_wasm_exec::jit_dispatch`.
803    let _ = writeln!(
804        text,
805        "    ld.param.u64 %rd0, [{entry}_param_in_ptr];",
806        entry = blueprint.entry,
807    );
808    let _ = writeln!(
809        text,
810        "    ld.param.u64 %rd1, [{entry}_param_out_ptr];",
811        entry = blueprint.entry,
812    );
813    let _ = writeln!(
814        text,
815        "    ld.param.u32 %r0, [{entry}_param_n];",
816        entry = blueprint.entry,
817    );
818    let _ = writeln!(text);
819
820    text.push_str(&body);
821
822    let _ = writeln!(text);
823    let _ = writeln!(text, "    ret;");
824    let _ = writeln!(text, "}}");
825
826    Ok(EmittedPtx {
827        text,
828        launch_geometry: (grid_size, block_size),
829    })
830}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835    use crate::ir::{GridHint, TensorWasmKernelBlueprint, TensorWasmOp};
836
837    #[test]
838    fn vector_add_emits_add_f32() {
839        let bp = TensorWasmKernelBlueprint::new("vector_add")
840            .push(TensorWasmOp::LoadUnified {
841                elem: ElemType::F32,
842                lanes: 4,
843            })
844            .push(TensorWasmOp::LoadUnified {
845                elem: ElemType::F32,
846                lanes: 4,
847            })
848            .push(TensorWasmOp::VecAdd {
849                elem: ElemType::F32,
850                lanes: 4,
851            })
852            .push(TensorWasmOp::StoreUnified {
853                elem: ElemType::F32,
854                lanes: 4,
855            });
856        let out = emit(&bp).expect("emit");
857        assert!(out.text.contains(".target sm_80"));
858        assert!(out.text.contains(".visible .entry vector_add"));
859        assert!(out.text.contains("add.f32"));
860        assert!(out.text.contains("ld.global.lu.f32"));
861        assert!(out.text.contains("st.global.cs.f32"));
862        assert!(out.text.contains("ret;"));
863    }
864
865    /// jit CRITICAL (finding 1): an `i32x4` blueprint must emit INTEGER PTX
866    /// (`add.s32` into `%r` registers, `.u32` loads/stores), never the
867    /// `add.f32` float kernel the emitter previously produced for every
868    /// SIMD shape. This is the regression test that pins "integer SIMD is
869    /// not lowered to a float kernel".
870    #[test]
871    fn i32x4_add_emits_integer_ops_not_float() {
872        let bp = TensorWasmKernelBlueprint::new("int_add")
873            .push(TensorWasmOp::LoadUnified {
874                elem: ElemType::I32,
875                lanes: 4,
876            })
877            .push(TensorWasmOp::LoadUnified {
878                elem: ElemType::I32,
879                lanes: 4,
880            })
881            .push(TensorWasmOp::VecAdd {
882                elem: ElemType::I32,
883                lanes: 4,
884            })
885            .push(TensorWasmOp::StoreUnified {
886                elem: ElemType::I32,
887                lanes: 4,
888            });
889        let out = emit(&bp).expect("i32x4 must emit");
890        // Integer add into the integer register class.
891        assert!(
892            out.text.contains("add.s32"),
893            "expected integer add, got:\n{}",
894            out.text
895        );
896        // The float add must NOT appear — that was the miscompile.
897        assert!(
898            !out.text.contains("add.f32"),
899            "i32x4.add must not lower to a float kernel:\n{}",
900            out.text
901        );
902        // Integer loads/stores use the `.u32` width form, and the integer
903        // register class `%r` must be declared above the reserved `%r0`.
904        assert!(out.text.contains("ld.global.lu.u32"));
905        assert!(out.text.contains("st.global.cs.u32"));
906        assert!(out.text.contains("%r1"));
907    }
908
909    /// jit CRITICAL (finding 1): `i32x4.mul` emits `mul.lo.s32`, not
910    /// `mul.f32`.
911    #[test]
912    fn i32x4_mul_emits_integer_mul() {
913        let bp = TensorWasmKernelBlueprint::new("int_mul").push(TensorWasmOp::VecMul {
914            elem: ElemType::I32,
915            lanes: 4,
916        });
917        let out = emit(&bp).expect("i32x4 mul must emit");
918        assert!(out.text.contains("mul.lo.s32"));
919        assert!(!out.text.contains("mul.f32"));
920    }
921
922    /// jit CRITICAL (finding 1): the lane count drives how many instruction
923    /// lines are emitted — an f64x2 op (which the emitter does NOT support
924    /// end-to-end) is refused rather than miscompiled, and a non-default
925    /// lane count is honoured exactly for the supported widths.
926    #[test]
927    fn unsupported_element_width_refused_not_miscompiled() {
928        // f64x2 add reaches the emitter only via the public API (the
929        // rewrite path fails it closed upstream). The emitter must refuse
930        // rather than emit `add.f32`.
931        let bp = TensorWasmKernelBlueprint::new("f64_add").push(TensorWasmOp::VecAdd {
932            elem: ElemType::F64,
933            lanes: 2,
934        });
935        assert!(matches!(emit(&bp), Err(EmitError::NotYetImplemented(_))));
936    }
937
938    /// MatMul lowering is deferred to v0.4. The emitter must refuse to
939    /// produce PTX for blueprints containing a `MatMul` op rather than
940    /// emit a syntactically-valid-but-semantically-broken wmma block
941    /// (the prior emitter referenced undefined `%r1..%r7` and never paired
942    /// the accumulators with a store, so launched kernels would silently
943    /// corrupt state). Callers should treat this as a deopt signal.
944    #[test]
945    fn matmul_emission_is_not_yet_implemented() {
946        let bp = TensorWasmKernelBlueprint::new("matmul_16x16x16").push(TensorWasmOp::MatMul {
947            m: 16,
948            n: 16,
949            k: 16,
950        });
951        let err = emit(&bp).expect_err("MatMul emission must fail until v0.4");
952        assert!(matches!(err, EmitError::NotYetImplemented(_)));
953    }
954
955    /// SAFETY-DEFAULT PIN: the default `EmitConfig` must keep refusing
956    /// MatMul. This locks in the constraint that the experimental wmma
957    /// lowering is opt-in only — a regression that flips the default on
958    /// would fail here before it could reach a GPU launch path.
959    #[test]
960    fn matmul_refused_under_default_config() {
961        let bp = TensorWasmKernelBlueprint::new("matmul_16x16x16").push(TensorWasmOp::MatMul {
962            m: 16,
963            n: 16,
964            k: 16,
965        });
966        // Default config: flag is off.
967        assert!(!EmitConfig::default().enable_experimental_matmul);
968        let err = emit_with(&bp, &EmitConfig::default())
969            .expect_err("MatMul must be refused with the flag off");
970        assert!(matches!(err, EmitError::NotYetImplemented(_)));
971        // The convenience `emit` wrapper uses the default config, so it
972        // must refuse too.
973        assert!(matches!(emit(&bp), Err(EmitError::NotYetImplemented(_))));
974    }
975
976    /// EXPERIMENTAL opt-in: with the flag on, an m16n16k16 MatMul lowers
977    /// to the wmma fragment-load → mma.sync → fragment-store sequence.
978    #[test]
979    fn matmul_opt_in_emits_wmma_sequence() {
980        let bp = TensorWasmKernelBlueprint::new("matmul").push(TensorWasmOp::MatMul {
981            m: 16,
982            n: 16,
983            k: 16,
984        });
985        let cfg = EmitConfig {
986            enable_experimental_matmul: true,
987            ..EmitConfig::default()
988        };
989        let out = emit_with(&bp, &cfg).expect("opt-in emit must succeed");
990        // Fragment loads for both operands + the accumulator.
991        assert!(out.text.contains("wmma.load.a.sync.aligned.row.m16n16k16"));
992        assert!(out.text.contains("wmma.load.b.sync.aligned.col.m16n16k16"));
993        assert!(out.text.contains("wmma.load.c.sync.aligned.row.m16n16k16"));
994        // Exactly one mma.sync for a single k16 tile.
995        assert_eq!(out.text.matches("wmma.mma.sync.aligned").count(), 1);
996        // Paired store of the accumulator fragment.
997        assert!(out.text.contains("wmma.store.d.sync.aligned.row.m16n16k16"));
998        // The operand-fragment register class must be declared.
999        assert!(out.text.contains(".reg .b32   %rb<"));
1000        // The prominent experimental marker rides in the body.
1001        assert!(out.text.contains("EXPERIMENTAL / UNVERIFIED ON HARDWARE"));
1002    }
1003
1004    /// Even with the flag on, only the modelled m16n16k16 shape is
1005    /// lowered — any other tile is still refused (never silently
1006    /// miscompiled).
1007    #[test]
1008    fn matmul_opt_in_rejects_non_16_shape() {
1009        let bp = TensorWasmKernelBlueprint::new("matmul").push(TensorWasmOp::MatMul {
1010            m: 32,
1011            n: 32,
1012            k: 32,
1013        });
1014        let cfg = EmitConfig {
1015            enable_experimental_matmul: true,
1016            ..EmitConfig::default()
1017        };
1018        assert!(matches!(
1019            emit_with(&bp, &cfg),
1020            Err(EmitError::NotYetImplemented(_))
1021        ));
1022    }
1023
1024    /// The default (flag-off) PTX must be byte-identical to before the
1025    /// experimental wmma support was added for non-MatMul blueprints: the
1026    /// `.b32 %rb` operand-fragment declaration only appears when a wmma
1027    /// MatMul is actually lowered.
1028    #[test]
1029    fn no_wmma_reg_decl_without_matmul() {
1030        let bp = TensorWasmKernelBlueprint::new("vector_add")
1031            .push(TensorWasmOp::LoadUnified {
1032                elem: ElemType::F32,
1033                lanes: 4,
1034            })
1035            .push(TensorWasmOp::LoadUnified {
1036                elem: ElemType::F32,
1037                lanes: 4,
1038            })
1039            .push(TensorWasmOp::VecAdd {
1040                elem: ElemType::F32,
1041                lanes: 4,
1042            })
1043            .push(TensorWasmOp::StoreUnified {
1044                elem: ElemType::F32,
1045                lanes: 4,
1046            });
1047        let out = emit(&bp).expect("emit");
1048        assert!(!out.text.contains("%rb<"));
1049        assert!(!out.text.contains("wmma."));
1050    }
1051
1052    /// MatMul anywhere in the op stream taints the whole blueprint —
1053    /// not just blueprints whose only op is MatMul.
1054    #[test]
1055    fn matmul_in_mixed_stream_also_refused() {
1056        let bp = TensorWasmKernelBlueprint::new("mixed")
1057            .push(TensorWasmOp::LoadUnified {
1058                elem: ElemType::F32,
1059                lanes: 4,
1060            })
1061            .push(TensorWasmOp::MatMul {
1062                m: 16,
1063                n: 16,
1064                k: 16,
1065            })
1066            .push(TensorWasmOp::StoreUnified {
1067                elem: ElemType::F32,
1068                lanes: 4,
1069            });
1070        assert!(matches!(emit(&bp), Err(EmitError::NotYetImplemented(_))));
1071    }
1072
1073    #[test]
1074    fn ptx_header_includes_version_and_target() {
1075        let bp = TensorWasmKernelBlueprint::new("noop");
1076        let out = emit(&bp).expect("emit");
1077        assert!(out.text.contains(".version 8.0"));
1078        assert!(out.text.contains(".target sm_80"));
1079        assert!(out.text.contains(".address_size 64"));
1080    }
1081
1082    #[test]
1083    fn launch_geometry_in_header() {
1084        let bp = TensorWasmKernelBlueprint::new("k").with_grid(GridHint {
1085            total_threads: 1024,
1086            preferred_block_size: 128,
1087        });
1088        let out = emit(&bp).expect("emit");
1089        assert_eq!(out.launch_geometry, (8, 128));
1090        assert!(out.text.contains(".maxntid 128, 1, 1"));
1091        // Negative assertion: the broken C++-annotation form must not appear.
1092        assert!(!out.text.contains("__launch_bounds__"));
1093    }
1094
1095    #[test]
1096    fn barrier_emits_bar_sync() {
1097        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::Barrier);
1098        let out = emit(&bp).expect("emit");
1099        assert!(out.text.contains("bar.sync 0;"));
1100    }
1101
1102    #[test]
1103    fn fma_emits_fma_rn() {
1104        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecFma {
1105            elem: ElemType::F32,
1106            lanes: 4,
1107        });
1108        let out = emit(&bp).expect("emit");
1109        assert!(out.text.contains("fma.rn.f32"));
1110    }
1111
1112    #[test]
1113    fn custom_target() {
1114        let bp = TensorWasmKernelBlueprint::new("k");
1115        let cfg = EmitConfig {
1116            target: "sm_89".into(),
1117            ..EmitConfig::default()
1118        };
1119        let out = emit_with(&bp, &cfg).expect("emit");
1120        assert!(out.text.contains(".target sm_89"));
1121    }
1122
1123    #[test]
1124    fn emitted_text_nonempty() {
1125        let bp = TensorWasmKernelBlueprint::new("k");
1126        let out = emit(&bp).expect("emit");
1127        assert!(!out.is_empty());
1128    }
1129
1130    /// Critical correctness assertion: each lane-op produces a *fresh*
1131    /// register. The old emitter cycled `%f0..%f3` which silently read
1132    /// undefined values after four ops. With proper allocation, an 8-lane
1133    /// add produces `%f0..%fN` with strictly increasing destinations.
1134    #[test]
1135    fn register_allocator_assigns_fresh_destination_per_op() {
1136        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
1137            elem: ElemType::F32,
1138            lanes: 8,
1139        });
1140        let out = emit(&bp).expect("emit");
1141        // After lowering, we expect at least registers %f0 through %f7 to
1142        // appear as add destinations. The exact regex match would couple
1143        // the test to the syntax, so we just count distinct destinations.
1144        let mut destinations = std::collections::BTreeSet::new();
1145        for line in out.text.lines() {
1146            if let Some(rest) = line.trim_start().strip_prefix("add.f32 %f") {
1147                if let Some((reg, _)) = rest.split_once(',') {
1148                    if let Ok(n) = reg.trim().parse::<u32>() {
1149                        destinations.insert(n);
1150                    }
1151                }
1152            }
1153        }
1154        assert!(
1155            destinations.len() >= 8,
1156            "expected at least 8 distinct add destinations, got {} ({:?})",
1157            destinations.len(),
1158            destinations,
1159        );
1160    }
1161
1162    #[test]
1163    fn register_declarations_match_usage() {
1164        // 16 lanes of add will need at least 16 fresh registers (plus the
1165        // operand registers materialised on stack-underflow). The header
1166        // `.reg .f32 %f<N>` must declare at least N regs.
1167        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
1168            elem: ElemType::F32,
1169            lanes: 16,
1170        });
1171        let out = emit(&bp).expect("emit");
1172        // Find the .reg .f32 declaration and parse out the count.
1173        let count_line = out
1174            .text
1175            .lines()
1176            .find(|l| l.contains(".reg .f32"))
1177            .expect("missing .reg .f32 declaration");
1178        let count: u32 = count_line
1179            .split('<')
1180            .nth(1)
1181            .and_then(|s| s.split('>').next())
1182            .and_then(|s| s.parse().ok())
1183            .expect("parse reg count");
1184        assert!(
1185            count >= 16,
1186            "register declaration must cover all uses (got {count})"
1187        );
1188    }
1189
1190    #[test]
1191    fn prologue_loads_params_into_registers() {
1192        let bp = TensorWasmKernelBlueprint::new("vec_op");
1193        let out = emit(&bp).expect("emit");
1194        assert!(out.text.contains("ld.param.u64 %rd0"));
1195        assert!(out.text.contains("ld.param.u64 %rd1"));
1196        assert!(out.text.contains("ld.param.u32 %r0"));
1197    }
1198
1199    #[test]
1200    fn loads_advance_input_offset() {
1201        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::LoadUnified {
1202            elem: ElemType::F32,
1203            lanes: 3,
1204        });
1205        let out = emit(&bp).expect("emit");
1206        // First lane at [%rd0], next at [%rd0+4], next at [%rd0+8].
1207        assert!(out.text.contains("[%rd0];"), "first load uses no offset");
1208        assert!(out.text.contains("[%rd0+4]"), "second lane at offset 4");
1209        assert!(out.text.contains("[%rd0+8]"), "third lane at offset 8");
1210    }
1211
1212    /// DoS guardrail (jit L2): a single op requesting more than `MAX_LANES`
1213    /// lanes is refused with the structured `EmissionBudgetExceeded` error
1214    /// before any PTX is produced. The public `push` API accepts arbitrary
1215    /// `lanes`, so without this the emitter would loop `0..u32::MAX`.
1216    #[test]
1217    fn oversized_lanes_is_rejected() {
1218        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
1219            elem: ElemType::F32,
1220            lanes: u32::MAX,
1221        });
1222        let err = emit(&bp).expect_err("oversized lanes must be refused");
1223        assert!(matches!(err, EmitError::EmissionBudgetExceeded { .. }));
1224        // Just over the per-op cap is also refused.
1225        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::LoadUnified {
1226            elem: ElemType::F32,
1227            lanes: MAX_LANES + 1,
1228        });
1229        assert!(matches!(
1230            emit(&bp),
1231            Err(EmitError::EmissionBudgetExceeded { .. })
1232        ));
1233    }
1234
1235    /// DoS guardrail (jit L2): even when each op is within `MAX_LANES`, a
1236    /// long enough op stream whose aggregate `ops * lanes` exceeds
1237    /// `MAX_EMITTED_OPS` is refused.
1238    #[test]
1239    fn aggregate_emission_budget_is_enforced() {
1240        // Each op is at the per-op cap; enough of them to blow the aggregate.
1241        let n_ops = (MAX_EMITTED_OPS / u64::from(MAX_LANES)) as usize + 2;
1242        let mut bp = TensorWasmKernelBlueprint::new("k");
1243        for _ in 0..n_ops {
1244            bp = bp.push(TensorWasmOp::VecMul {
1245                elem: ElemType::F32,
1246                lanes: MAX_LANES,
1247            });
1248        }
1249        assert!(matches!(
1250            emit(&bp),
1251            Err(EmitError::EmissionBudgetExceeded { .. })
1252        ));
1253    }
1254
1255    /// A blueprint at exactly the `MAX_LANES` per-op bound (and well under
1256    /// the aggregate cap) still emits successfully — the guard rejects only
1257    /// what is over budget.
1258    #[test]
1259    fn lanes_at_max_still_emits() {
1260        let bp = TensorWasmKernelBlueprint::new("k").push(TensorWasmOp::VecAdd {
1261            elem: ElemType::F32,
1262            lanes: MAX_LANES,
1263        });
1264        assert!(emit(&bp).is_ok());
1265    }
1266
1267    #[test]
1268    fn stores_advance_output_offset() {
1269        let bp = TensorWasmKernelBlueprint::new("k")
1270            .push(TensorWasmOp::LoadUnified {
1271                elem: ElemType::F32,
1272                lanes: 2,
1273            })
1274            .push(TensorWasmOp::StoreUnified {
1275                elem: ElemType::F32,
1276                lanes: 2,
1277            });
1278        let out = emit(&bp).expect("emit");
1279        assert!(out.text.contains("[%rd1]"));
1280        assert!(out.text.contains("[%rd1+4]"));
1281    }
1282}