oxicuda_driver/module.rs
1//! PTX module loading and kernel function management.
2//!
3//! Modules are created from PTX source code and contain one or more
4//! kernel functions that can be launched on the GPU.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! # use oxicuda_driver::module::{Module, JitOptions};
10//! # fn main() -> Result<(), oxicuda_driver::error::CudaError> {
11//! let ptx = r#"
12//! .version 7.0
13//! .target sm_70
14//! .address_size 64
15//! .visible .entry my_kernel() { ret; }
16//! "#;
17//!
18//! let module = Module::from_ptx(ptx)?;
19//! let func = module.get_function("my_kernel")?;
20//!
21//! // Or with JIT options and compilation logs:
22//! let opts = JitOptions { optimization_level: 4, ..Default::default() };
23//! let (module2, log) = Module::from_ptx_with_options(ptx, &opts)?;
24//! if !log.info.is_empty() {
25//! println!("JIT info: {}", log.info);
26//! }
27//! # Ok(())
28//! # }
29//! ```
30
31use std::ffi::{CString, c_void};
32
33use crate::error::{CudaError, CudaResult};
34use crate::ffi::{CUfunction, CUjit_option, CUmodule};
35use crate::loader::try_driver;
36
37// ---------------------------------------------------------------------------
38// JitOptions
39// ---------------------------------------------------------------------------
40
41/// Options for JIT compilation of PTX to GPU binary.
42///
43/// These options control the behaviour of the CUDA JIT compiler when
44/// loading PTX source via [`Module::from_ptx_with_options`].
45#[derive(Debug, Clone)]
46pub struct JitOptions {
47 /// Maximum number of registers per thread (0 = no limit).
48 ///
49 /// Limiting register usage can increase occupancy at the cost of
50 /// potential register spilling to local memory.
51 pub max_registers: u32,
52 /// Optimisation level (0--4, default 4).
53 ///
54 /// Higher levels produce faster code but take longer to compile.
55 pub optimization_level: u32,
56 /// Whether to generate debug information in the compiled binary.
57 pub generate_debug_info: bool,
58 /// If `true`, the JIT compiler determines the target compute
59 /// capability from the current CUDA context.
60 pub target_from_context: bool,
61}
62
63impl Default for JitOptions {
64 /// Returns sensible defaults: no register limit, maximum
65 /// optimisation, no debug info, target derived from context.
66 fn default() -> Self {
67 Self {
68 max_registers: 0,
69 optimization_level: 4,
70 generate_debug_info: false,
71 target_from_context: true,
72 }
73 }
74}
75
76// ---------------------------------------------------------------------------
77// JitLog
78// ---------------------------------------------------------------------------
79
80/// Severity of a JIT compiler diagnostic message.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum JitSeverity {
83 /// A fatal error that prevents PTX compilation.
84 Fatal,
85 /// A non-fatal error.
86 Error,
87 /// A compiler warning (compilation may still succeed).
88 Warning,
89 /// An informational message (e.g. register usage).
90 Info,
91}
92
93impl std::fmt::Display for JitSeverity {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Fatal => f.write_str("fatal"),
97 Self::Error => f.write_str("error"),
98 Self::Warning => f.write_str("warning"),
99 Self::Info => f.write_str("info"),
100 }
101 }
102}
103
104/// A single structured diagnostic emitted by the JIT compiler.
105///
106/// Parsed from the raw `ptxas` log lines that look like:
107///
108/// ```text
109/// ptxas error : 'kernel', line 10; error : Unknown instruction 'xyz'
110/// ptxas warning : 'kernel', line 15; warning : double-precision is slow
111/// ptxas info : 'kernel' used 16 registers, 0 bytes smem
112/// ```
113#[derive(Debug, Clone)]
114pub struct JitDiagnostic {
115 /// Severity level.
116 pub severity: JitSeverity,
117 /// Kernel function name, if the message is function-scoped.
118 pub kernel: Option<String>,
119 /// Source line number, if present.
120 pub line: Option<u32>,
121 /// Human-readable message text.
122 pub message: String,
123}
124
125/// Log output from JIT compilation.
126///
127/// After calling [`Module::from_ptx_with_options`], this struct
128/// contains any informational or error messages emitted by the
129/// JIT compiler.
130///
131/// Use [`JitLog::parse_diagnostics`] to obtain structured
132/// [`JitDiagnostic`] entries instead of parsing the raw strings.
133#[derive(Debug, Clone, Default)]
134pub struct JitLog {
135 /// Informational messages from the JIT compiler.
136 pub info: String,
137 /// Error messages from the JIT compiler.
138 pub error: String,
139}
140
141impl JitLog {
142 /// Returns `true` if there are no messages in either buffer.
143 #[must_use]
144 pub fn is_empty(&self) -> bool {
145 self.info.is_empty() && self.error.is_empty()
146 }
147
148 /// Returns `true` if the error buffer is non-empty.
149 #[must_use]
150 pub fn has_errors(&self) -> bool {
151 !self.error.is_empty()
152 }
153
154 /// Parse both log buffers into a `Vec` of structured [`JitDiagnostic`]
155 /// entries.
156 ///
157 /// Lines that do not match the `ptxas` diagnostic format are included as
158 /// [`JitSeverity::Info`] diagnostics with no kernel or line information,
159 /// unless they are entirely blank.
160 ///
161 /// # Message format
162 ///
163 /// The CUDA JIT compiler emits lines in one of these formats:
164 ///
165 /// ```text
166 /// ptxas {severity} : '{kernel}', line {n}; {type} : {message}
167 /// ptxas {severity} : '{kernel}' {message}
168 /// ptxas {severity} : {message}
169 /// ```
170 ///
171 /// This method normalises all of those into [`JitDiagnostic`] values.
172 #[must_use]
173 pub fn parse_diagnostics(&self) -> Vec<JitDiagnostic> {
174 let mut out = Vec::new();
175 for line in self.error.lines().chain(self.info.lines()) {
176 if let Some(d) = parse_ptxas_line(line) {
177 out.push(d);
178 }
179 }
180 out
181 }
182
183 /// Return only the [`JitDiagnostic`] entries whose severity is
184 /// [`JitSeverity::Error`] or [`JitSeverity::Fatal`].
185 #[must_use]
186 pub fn errors(&self) -> Vec<JitDiagnostic> {
187 self.parse_diagnostics()
188 .into_iter()
189 .filter(|d| matches!(d.severity, JitSeverity::Error | JitSeverity::Fatal))
190 .collect()
191 }
192
193 /// Return only the [`JitDiagnostic`] entries whose severity is
194 /// [`JitSeverity::Warning`].
195 #[must_use]
196 pub fn warnings(&self) -> Vec<JitDiagnostic> {
197 self.parse_diagnostics()
198 .into_iter()
199 .filter(|d| matches!(d.severity, JitSeverity::Warning))
200 .collect()
201 }
202}
203
204// ── ptxas log line parser ─────────────────────────────────────────────────────
205
206/// Parse a single `ptxas` log line into a [`JitDiagnostic`], returning
207/// `None` for blank lines.
208///
209/// Handles these representative patterns:
210///
211/// ```text
212/// ptxas error : 'vec_add', line 10; error : Unknown instruction 'xyz'
213/// ptxas warning : 'vec_add', line 15; warning : slow double-precision
214/// ptxas info : 'vec_add' used 16 registers, 0 bytes smem, 0 bytes cmem[0]
215/// ptxas fatal : Unresolved extern function 'foo'
216/// ```
217fn parse_ptxas_line(line: &str) -> Option<JitDiagnostic> {
218 let line = line.trim();
219 if line.is_empty() {
220 return None;
221 }
222
223 // Must start with "ptxas " (case-insensitive is not needed — ptxas always
224 // uses lower-case).
225 let rest = line.strip_prefix("ptxas ")?;
226
227 // Extract severity word (first whitespace-delimited token).
228 let (sev_str, after_sev) = split_first_word(rest.trim_start());
229 let severity = match sev_str.to_ascii_lowercase().trim_end_matches(':') {
230 "fatal" => JitSeverity::Fatal,
231 "error" => JitSeverity::Error,
232 "warning" => JitSeverity::Warning,
233 "info" => JitSeverity::Info,
234 _ => JitSeverity::Info,
235 };
236
237 // Skip past `: ` after the severity keyword.
238 let body = skip_colon(after_sev.trim_start());
239
240 // Try to extract kernel name from `'kernel_name'` at the start.
241 let (kernel, after_kernel) = extract_kernel_name(body);
242
243 // Try to extract line number: `, line N;` or `, line N,`.
244 let (line_no, after_line) = extract_line_number(after_kernel);
245
246 // The remaining text — skip a leading type word if present (e.g. `error : `).
247 let message = extract_message(after_line.trim());
248
249 Some(JitDiagnostic {
250 severity,
251 kernel,
252 line: line_no,
253 message: message.to_string(),
254 })
255}
256
257/// Split a `&str` at the first whitespace boundary; returns `("", s)` if
258/// there is no whitespace.
259fn split_first_word(s: &str) -> (&str, &str) {
260 match s.find(|c: char| c.is_whitespace()) {
261 Some(pos) => (&s[..pos], &s[pos..]),
262 None => (s, ""),
263 }
264}
265
266/// Skip past the first `: ` (colon + optional spaces) in `s`.
267fn skip_colon(s: &str) -> &str {
268 if let Some(pos) = s.find(':') {
269 s[pos + 1..].trim_start()
270 } else {
271 s
272 }
273}
274
275/// Attempt to extract `'kernel_name'` from the beginning of `s`.
276/// Returns `(Some(name), rest_after_name)` or `(None, s)`.
277fn extract_kernel_name(s: &str) -> (Option<String>, &str) {
278 let s = s.trim_start();
279 if !s.starts_with('\'') {
280 return (None, s);
281 }
282 let inner = &s[1..];
283 if let Some(end) = inner.find('\'') {
284 let name = inner[..end].to_string();
285 let after = &inner[end + 1..];
286 (Some(name), after)
287 } else {
288 (None, s)
289 }
290}
291
292/// Attempt to extract `, line N;` or `, line N,` from the start of `s`.
293/// Returns `(Some(n), rest)` or `(None, s)`.
294fn extract_line_number(s: &str) -> (Option<u32>, &str) {
295 // Accept `, line N` (with optional trailing `;` or `,`)
296 let s_trim = s.trim_start_matches([',', ' ', ';']);
297 let lower = s_trim.to_ascii_lowercase();
298 if !lower.starts_with("line ") {
299 return (None, s);
300 }
301 let after_line = &s_trim[5..]; // skip "line "
302 let (num_str, rest) = split_first_word(after_line.trim_start());
303 let num_clean: String = num_str.chars().filter(|c| c.is_ascii_digit()).collect();
304 if let Ok(n) = num_clean.parse::<u32>() {
305 (Some(n), rest)
306 } else {
307 (None, s)
308 }
309}
310
311/// Strip a leading `type : ` prefix (e.g. `error : ` or `warning : `)
312/// from a message if present; return the remaining text.
313fn extract_message(s: &str) -> &str {
314 // Pattern: word followed by optional spaces and `:`.
315 let (word, rest) = split_first_word(s);
316 let word_clean = word.trim_end_matches(':');
317 if matches!(
318 word_clean.to_ascii_lowercase().as_str(),
319 "error" | "warning" | "info" | "fatal"
320 ) {
321 skip_colon(rest.trim_start())
322 } else {
323 s
324 }
325}
326
327// ---------------------------------------------------------------------------
328// Module
329// ---------------------------------------------------------------------------
330
331/// A loaded CUDA module containing one or more kernel functions.
332///
333/// Modules are typically created from PTX source via [`Module::from_ptx`]
334/// or [`Module::from_ptx_with_options`]. Individual kernel functions
335/// are retrieved by name with [`Module::get_function`].
336///
337/// The module is unloaded when this struct is dropped.
338pub struct Module {
339 /// Raw CUDA module handle.
340 raw: CUmodule,
341}
342
343// SAFETY: CUDA modules are safe to send between threads when properly
344// synchronised via the driver API.
345unsafe impl Send for Module {}
346
347/// Size of the JIT log buffers in bytes.
348const JIT_LOG_BUFFER_SIZE: usize = 4096;
349
350impl Module {
351 /// Loads a module from PTX source with default JIT options.
352 ///
353 /// The PTX string is automatically null-terminated before being
354 /// passed to the driver.
355 ///
356 /// # Errors
357 ///
358 /// Returns [`CudaError::InvalidImage`] if
359 /// the PTX is malformed, or another [`CudaError`] if the driver
360 /// call fails (e.g. no current context).
361 pub fn from_ptx(ptx: &str) -> CudaResult<Self> {
362 let api = try_driver()?;
363 let c_ptx = CString::new(ptx).map_err(|_| CudaError::InvalidValue)?;
364 let mut raw = CUmodule::default();
365 crate::cuda_call!((api.cu_module_load_data)(
366 &mut raw,
367 c_ptx.as_ptr().cast::<c_void>()
368 ))?;
369 Ok(Self { raw })
370 }
371
372 /// Loads a module from PTX source with explicit JIT compiler options.
373 ///
374 /// Returns the loaded module together with a [`JitLog`] containing
375 /// any informational or error messages from the JIT compiler.
376 ///
377 /// # Errors
378 ///
379 /// Returns a [`CudaError`] if JIT compilation fails or the driver
380 /// call otherwise errors.
381 pub fn from_ptx_with_options(ptx: &str, options: &JitOptions) -> CudaResult<(Self, JitLog)> {
382 let api = try_driver()?;
383 let c_ptx = CString::new(ptx).map_err(|_| CudaError::InvalidValue)?;
384
385 // Allocate log buffers on the heap.
386 let mut info_buf: Vec<u8> = vec![0u8; JIT_LOG_BUFFER_SIZE];
387 let mut error_buf: Vec<u8> = vec![0u8; JIT_LOG_BUFFER_SIZE];
388
389 // Build the parallel option-key and option-value arrays.
390 //
391 // Each option is a (CUjit_option, *mut c_void) pair. The value
392 // pointer is reinterpreted according to the option key — scalar
393 // values are cast directly to pointer-width integers.
394 let mut opt_keys: Vec<CUjit_option> = Vec::with_capacity(8);
395 let mut opt_vals: Vec<*mut c_void> = Vec::with_capacity(8);
396
397 // Info log buffer.
398 opt_keys.push(CUjit_option::InfoLogBuffer);
399 opt_vals.push(info_buf.as_mut_ptr().cast::<c_void>());
400
401 opt_keys.push(CUjit_option::InfoLogBufferSizeBytes);
402 opt_vals.push(JIT_LOG_BUFFER_SIZE as *mut c_void);
403
404 // Error log buffer.
405 opt_keys.push(CUjit_option::ErrorLogBuffer);
406 opt_vals.push(error_buf.as_mut_ptr().cast::<c_void>());
407
408 opt_keys.push(CUjit_option::ErrorLogBufferSizeBytes);
409 opt_vals.push(JIT_LOG_BUFFER_SIZE as *mut c_void);
410
411 // Optimisation level.
412 opt_keys.push(CUjit_option::OptimizationLevel);
413 opt_vals.push(options.optimization_level as *mut c_void);
414
415 // Max registers (only if non-zero to avoid overriding defaults).
416 if options.max_registers > 0 {
417 opt_keys.push(CUjit_option::MaxRegisters);
418 opt_vals.push(options.max_registers as *mut c_void);
419 }
420
421 // Generate debug info.
422 if options.generate_debug_info {
423 opt_keys.push(CUjit_option::GenerateDebugInfo);
424 opt_vals.push(core::ptr::without_provenance_mut::<c_void>(1));
425 }
426
427 // Target from context.
428 if options.target_from_context {
429 opt_keys.push(CUjit_option::TargetFromCuContext);
430 opt_vals.push(core::ptr::without_provenance_mut::<c_void>(1));
431 }
432
433 let num_options = opt_keys.len() as u32;
434
435 let mut raw = CUmodule::default();
436 let result = crate::cuda_call!((api.cu_module_load_data_ex)(
437 &mut raw,
438 c_ptx.as_ptr().cast::<c_void>(),
439 num_options,
440 opt_keys.as_mut_ptr(),
441 opt_vals.as_mut_ptr(),
442 ));
443
444 // Extract log strings regardless of success or failure.
445 let log = JitLog {
446 info: buf_to_string(&info_buf),
447 error: buf_to_string(&error_buf),
448 };
449
450 result?;
451 Ok((Self { raw }, log))
452 }
453
454 /// Retrieves a kernel function by name from this module.
455 ///
456 /// The returned [`Function`] is a lightweight handle. The caller
457 /// must ensure that this `Module` outlives any `Function` handles
458 /// obtained from it.
459 ///
460 /// # Errors
461 ///
462 /// Returns [`CudaError::NotFound`] if no
463 /// function with the given name exists in the module, or another
464 /// [`CudaError`] on driver failure.
465 pub fn get_function(&self, name: &str) -> CudaResult<Function> {
466 let api = try_driver()?;
467 let c_name = CString::new(name).map_err(|_| CudaError::InvalidValue)?;
468 let mut raw = CUfunction::default();
469 crate::cuda_call!((api.cu_module_get_function)(
470 &mut raw,
471 self.raw,
472 c_name.as_ptr()
473 ))?;
474 Ok(Function { raw })
475 }
476
477 /// Returns the raw [`CUmodule`] handle.
478 ///
479 /// # Safety (caller)
480 ///
481 /// The caller must not unload or otherwise invalidate the handle
482 /// while this `Module` is still alive.
483 #[inline]
484 pub fn raw(&self) -> CUmodule {
485 self.raw
486 }
487}
488
489impl Drop for Module {
490 fn drop(&mut self) {
491 if let Ok(api) = try_driver() {
492 let rc = unsafe { (api.cu_module_unload)(self.raw) };
493 if rc != 0 {
494 tracing::warn!(
495 cuda_error = rc,
496 module = ?self.raw,
497 "cuModuleUnload failed during drop"
498 );
499 }
500 }
501 }
502}
503
504// ---------------------------------------------------------------------------
505// Function
506// ---------------------------------------------------------------------------
507
508/// A kernel function handle within a loaded module.
509///
510/// Functions are lightweight handles (a single pointer) — the lifetime
511/// is tied to the parent [`Module`]. The caller is responsible for
512/// ensuring the `Module` outlives any `Function` handles obtained
513/// from it.
514///
515/// Occupancy query methods are provided in the [`crate::occupancy`]
516/// module via an `impl Function` block.
517#[derive(Debug, Clone, Copy)]
518pub struct Function {
519 /// Raw CUDA function handle.
520 raw: CUfunction,
521}
522
523impl Function {
524 /// Returns the raw [`CUfunction`] handle.
525 ///
526 /// This is needed for kernel launches and occupancy queries
527 /// at the FFI level.
528 #[inline]
529 pub fn raw(&self) -> CUfunction {
530 self.raw
531 }
532}
533
534// ---------------------------------------------------------------------------
535// Helpers
536// ---------------------------------------------------------------------------
537
538/// Converts a null-terminated C buffer to a Rust [`String`], trimming
539/// trailing null bytes and whitespace.
540fn buf_to_string(buf: &[u8]) -> String {
541 // Find the first null byte (or use the whole buffer).
542 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
543 String::from_utf8_lossy(&buf[..len]).trim().to_string()
544}
545
546// ---------------------------------------------------------------------------
547// Tests
548// ---------------------------------------------------------------------------
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 // ── parse_ptxas_line ──────────────────────────────────────────────────────
555
556 #[test]
557 fn parse_blank_line_returns_none() {
558 assert!(parse_ptxas_line("").is_none());
559 assert!(parse_ptxas_line(" ").is_none());
560 }
561
562 #[test]
563 fn parse_non_ptxas_line_returns_none() {
564 // Lines not starting with "ptxas " are ignored.
565 assert!(parse_ptxas_line("nvcc error: something").is_none());
566 assert!(parse_ptxas_line(" error: foo").is_none());
567 }
568
569 #[test]
570 fn parse_standard_error_with_kernel_and_line() {
571 let line = "ptxas error : 'vec_add', line 42; error : Unknown instruction 'xyz.f32'";
572 let d = parse_ptxas_line(line).expect("should parse");
573 assert_eq!(d.severity, JitSeverity::Error);
574 assert_eq!(d.kernel.as_deref(), Some("vec_add"));
575 assert_eq!(d.line, Some(42));
576 assert!(
577 d.message.contains("Unknown instruction"),
578 "msg: {}",
579 d.message
580 );
581 }
582
583 #[test]
584 fn parse_warning_with_kernel_and_line() {
585 let line = "ptxas warning : 'my_kernel', line 7; warning : Double-precision instructions will be slow";
586 let d = parse_ptxas_line(line).expect("should parse");
587 assert_eq!(d.severity, JitSeverity::Warning);
588 assert_eq!(d.kernel.as_deref(), Some("my_kernel"));
589 assert_eq!(d.line, Some(7));
590 assert!(d.message.contains("Double-precision"), "msg: {}", d.message);
591 }
592
593 #[test]
594 fn parse_info_register_usage() {
595 let line =
596 "ptxas info : 'reduce_kernel' used 32 registers, 0 bytes smem, 0 bytes cmem[0]";
597 let d = parse_ptxas_line(line).expect("should parse");
598 assert_eq!(d.severity, JitSeverity::Info);
599 assert_eq!(d.kernel.as_deref(), Some("reduce_kernel"));
600 assert!(d.message.contains("32 registers"), "msg: {}", d.message);
601 assert!(d.line.is_none());
602 }
603
604 #[test]
605 fn parse_fatal_no_kernel() {
606 let line = "ptxas fatal : Unresolved extern function 'missing_func'";
607 let d = parse_ptxas_line(line).expect("should parse");
608 assert_eq!(d.severity, JitSeverity::Fatal);
609 assert!(d.kernel.is_none());
610 assert!(d.message.contains("Unresolved"), "msg: {}", d.message);
611 }
612
613 #[test]
614 fn parse_error_no_kernel_no_line() {
615 let line = "ptxas error : syntax error near token ';'";
616 let d = parse_ptxas_line(line).expect("should parse");
617 assert_eq!(d.severity, JitSeverity::Error);
618 assert!(d.kernel.is_none());
619 assert!(d.line.is_none());
620 assert!(d.message.contains("syntax error"), "msg: {}", d.message);
621 }
622
623 // ── JitLog helpers ────────────────────────────────────────────────────────
624
625 #[test]
626 fn jitlog_is_empty_for_default() {
627 let log = JitLog::default();
628 assert!(log.is_empty());
629 assert!(!log.has_errors());
630 }
631
632 #[test]
633 fn jitlog_has_errors_when_error_buf_nonempty() {
634 let log = JitLog {
635 info: String::new(),
636 error: "ptxas error : something went wrong".to_string(),
637 };
638 assert!(log.has_errors());
639 assert!(!log.is_empty());
640 }
641
642 #[test]
643 fn jitlog_parse_diagnostics_multiline() {
644 let log = JitLog {
645 error: concat!(
646 "ptxas error : 'k1', line 5; error : bad opcode\n",
647 "ptxas warning : 'k1', line 8; warning : slow path\n",
648 )
649 .to_string(),
650 info: "ptxas info : 'k1' used 8 registers, 0 bytes smem\n".to_string(),
651 };
652 let diags = log.parse_diagnostics();
653 assert_eq!(diags.len(), 3);
654 assert_eq!(diags[0].severity, JitSeverity::Error);
655 assert_eq!(diags[1].severity, JitSeverity::Warning);
656 assert_eq!(diags[2].severity, JitSeverity::Info);
657 }
658
659 #[test]
660 fn jitlog_errors_filter() {
661 let log = JitLog {
662 error: concat!(
663 "ptxas error : 'k', line 1; error : bad\n",
664 "ptxas warning : 'k', line 2; warning : slow\n",
665 )
666 .to_string(),
667 info: "ptxas info : 'k' used 4 registers\n".to_string(),
668 };
669 let errs = log.errors();
670 assert_eq!(errs.len(), 1);
671 assert_eq!(errs[0].severity, JitSeverity::Error);
672 }
673
674 #[test]
675 fn jitlog_warnings_filter() {
676 let log = JitLog {
677 error: "ptxas warning : 'k', line 3; warning : something slow\n".to_string(),
678 info: String::new(),
679 };
680 let warns = log.warnings();
681 assert_eq!(warns.len(), 1);
682 assert_eq!(warns[0].severity, JitSeverity::Warning);
683 assert_eq!(warns[0].line, Some(3));
684 }
685
686 // ── buf_to_string ─────────────────────────────────────────────────────────
687
688 #[test]
689 fn buf_to_string_null_terminated() {
690 let mut buf = b"hello\0\0\0".to_vec();
691 buf.extend_from_slice(&[0u8; 100]);
692 assert_eq!(buf_to_string(&buf), "hello");
693 }
694
695 #[test]
696 fn buf_to_string_empty() {
697 assert_eq!(buf_to_string(&[0u8; 10]), "");
698 }
699
700 #[test]
701 fn buf_to_string_no_null() {
702 let buf = b"abc".to_vec();
703 assert_eq!(buf_to_string(&buf), "abc");
704 }
705
706 // ── JitSeverity Display ───────────────────────────────────────────────────
707
708 #[test]
709 fn jit_severity_display() {
710 assert_eq!(JitSeverity::Fatal.to_string(), "fatal");
711 assert_eq!(JitSeverity::Error.to_string(), "error");
712 assert_eq!(JitSeverity::Warning.to_string(), "warning");
713 assert_eq!(JitSeverity::Info.to_string(), "info");
714 }
715}