use cutile_compiler::ast::Module;
use cutile_compiler::compiler::{CUDATileFunctionCompiler, CUDATileModules};
use cutile_compiler::cuda_tile_runtime_utils::get_gpu_name;
use cutile_compiler::error::JITError;
use cutile;
mod common;
const FORBIDDEN_INTERNALS: &[&str] = &[
"TileRustValue",
"TileRustType",
"TypeMeta",
"Kind::Compound",
"Kind::Struct",
"Kind::PrimitiveType",
"Kind::StructuredType",
"Kind::String",
"get_concrete_op_ident_from_types",
];
fn assert_no_internal_leaks(text: &str, context: &str) {
for &forbidden in FORBIDDEN_INTERNALS {
assert!(
!text.contains(forbidden),
"{context}: error message must not expose internal name `{forbidden}`.\n \
Full message: {text}"
);
}
}
fn assert_single_error_prefix(text: &str, context: &str) {
assert!(
text.starts_with("error: "),
"{context}: outer Error output must start with 'error: '.\n Got: {text}"
);
assert!(
!text.starts_with("error: error: "),
"{context}: 'error: ' prefix is doubled.\n Full message: {text}"
);
}
fn assert_jit_error_has_no_prefix(err: &JITError, context: &str) {
let output = format!("{err}");
assert!(
!output.starts_with("error: "),
"{context}: JITError must NOT start with 'error: ' — that prefix \
belongs to the outer Error type.\n Got: {output}"
);
}
fn assert_display_eq_debug_jit(err: &JITError, context: &str) {
let display = format!("{err}");
let debug = format!("{err:?}");
assert_eq!(
display, debug,
"{context}: Display and Debug must be identical.\n Display: {display}\n Debug: {debug}"
);
}
fn assert_display_eq_debug_outer(err: &cutile::error::Error, context: &str) {
let display = format!("{err}");
let debug = format!("{err:?}");
assert_eq!(
display, debug,
"{context}: Display and Debug must be identical.\n Display: {display}\n Debug: {debug}"
);
}
#[cutile::module]
mod error_quality_untyped_literal {
use cutile::core::*;
#[cutile::entry()]
fn untyped_kernel<const S: [i32; 1]>(output: &mut Tensor<f32, S>) {
let _x = 42;
let tile = load_tile_mut(output);
output.store(tile);
}
}
fn compile_and_get_error(
module_asts: Vec<Module>,
module_name: &str,
function_name: &str,
) -> JITError {
let modules = CUDATileModules::new(module_asts).expect("Failed to create CUDATileModules");
let gpu_name = get_gpu_name(0);
let compiler = CUDATileFunctionCompiler::new(
&modules,
module_name,
function_name,
&[128.to_string()],
&[("output", &[1])],
None,
gpu_name,
)
.expect("Compiler construction should succeed");
let result = compiler.compile();
let err = match result {
Err(e) => Some(e),
Ok(_) => None,
};
err.unwrap_or_else(|| {
panic!("Expected compilation of {module_name}::{function_name} to fail, but it succeeded.")
})
}
#[test]
fn untyped_literal_error_message_quality() {
common::with_test_stack(|| {
let err = compile_and_get_error(
error_quality_untyped_literal::_module_asts(),
"error_quality_untyped_literal",
"untyped_kernel",
);
let display = format!("{err}");
let debug = format!("{err:?}");
println!("=== UNTYPED LITERAL ERROR ===\n{display}\n");
assert_no_internal_leaks(&display, "untyped literal (Display)");
assert_no_internal_leaks(&debug, "untyped literal (Debug)");
assert_display_eq_debug_jit(&err, "untyped literal");
assert_jit_error_has_no_prefix(&err, "untyped literal");
assert!(
display.contains("42")
|| display.contains("type")
|| display.contains("annotation")
|| display.contains("literal"),
"Error message should reference the literal or suggest a type annotation.\n \
Got: {display}"
);
match &err {
JITError::Located(msg, loc) => {
assert!(
loc.is_known(),
"Untyped literal error should have a known source location, got: {loc:?}"
);
assert!(
loc.file.ends_with("error_quality.rs"),
"Expected file ending with 'error_quality.rs', got: {}",
loc.file
);
assert!(
display.contains("-->"),
"Located error with known location must include '-->' pointer.\n Got: {display}"
);
assert_no_internal_leaks(msg, "untyped literal (Located msg)");
}
JITError::Generic(msg) => {
assert_no_internal_leaks(msg, "untyped literal (Generic msg)");
}
_ => {
assert_no_internal_leaks(&display, "untyped literal (other variant)");
}
}
let outer: cutile::error::Error = err.into();
let outer_display = format!("{outer}");
assert_single_error_prefix(&outer_display, "untyped literal (outer)");
});
}
#[test]
fn outer_error_wrapping_jit_error_formatting() {
common::with_test_stack(|| {
use cutile_compiler::ast::SourceLocation;
let jit_generic = JITError::Generic("something went wrong".into());
assert_jit_error_has_no_prefix(&jit_generic, "JIT(Generic) bare");
let jit_display = format!("{jit_generic}");
let outer: cutile::error::Error = jit_generic.into();
let outer_display = format!("{outer}");
let outer_debug = format!("{outer:?}");
assert_single_error_prefix(&outer_display, "outer Error::JIT(Generic)");
assert_eq!(
outer_display, outer_debug,
"Outer Error Display and Debug must be identical for JIT(Generic).\n \
Display: {outer_display}\n Debug: {outer_debug}"
);
assert_eq!(
outer_display,
format!("error: {jit_display}"),
"Outer Error should be 'error: ' + inner JITError Display.\n \
Outer: {outer_display}\n Expected: error: {jit_display}"
);
assert_no_internal_leaks(&outer_display, "outer Error::JIT(Generic)");
let loc = SourceLocation::new("test.rs".into(), 10, 5);
let jit_located = JITError::Located("type mismatch".into(), loc);
assert_jit_error_has_no_prefix(&jit_located, "JIT(Located known) bare");
let jit_display = format!("{jit_located}");
let outer: cutile::error::Error = jit_located.into();
let outer_display = format!("{outer}");
let outer_debug = format!("{outer:?}");
assert_single_error_prefix(&outer_display, "outer Error::JIT(Located known)");
assert_eq!(
outer_display,
format!("error: {jit_display}"),
"Outer Error should be 'error: ' + inner JITError Display.\n \
Outer: {outer_display}\n Expected: error: {jit_display}"
);
assert_eq!(
outer_display, outer_debug,
"Outer Error Display and Debug must be identical for JIT(Located known).\n \
Display: {outer_display}\n Debug: {outer_debug}"
);
assert!(
outer_display.contains("-->"),
"Located error with known location must include '-->' in outer Error.\n \
Got: {outer_display}"
);
let loc_unknown = SourceLocation::unknown();
let jit_located_unknown = JITError::Located("some problem".into(), loc_unknown);
assert_jit_error_has_no_prefix(&jit_located_unknown, "JIT(Located unknown) bare");
let jit_display_unknown = format!("{jit_located_unknown}");
let outer: cutile::error::Error = jit_located_unknown.into();
let outer_display = format!("{outer}");
assert_single_error_prefix(&outer_display, "outer Error::JIT(Located unknown)");
assert_eq!(
outer_display,
format!("error: {jit_display_unknown}"),
"Outer Error should be 'error: ' + inner JITError Display (unknown loc).\n \
Outer: {outer_display}\n Expected: error: {jit_display_unknown}"
);
assert!(
!outer_display.contains("-->"),
"Located error with unknown location must NOT include '-->'.\n \
Got: {outer_display}"
);
let tensor_err = cutile::error::tensor_error("shape mismatch: expected [128], got [64]");
let tensor_display = format!("{tensor_err}");
let tensor_debug = format!("{tensor_err:?}");
assert_display_eq_debug_outer(&tensor_err, "Tensor");
assert_single_error_prefix(&tensor_display, "outer Error::Tensor");
assert_eq!(
tensor_display, tensor_debug,
"Outer Error Display and Debug must be identical for Tensor.\n \
Display: {tensor_display}\n Debug: {tensor_debug}"
);
let launch_err =
cutile::error::kernel_launch_error("grid dimensions exceed hardware limits");
let launch_display = format!("{launch_err}");
let launch_debug = format!("{launch_err:?}");
assert_display_eq_debug_outer(&launch_err, "KernelLaunch");
assert_single_error_prefix(&launch_display, "outer Error::KernelLaunch");
assert_eq!(
launch_display, launch_debug,
"Outer Error Display and Debug must be identical for KernelLaunch.\n \
Display: {launch_display}\n Debug: {launch_debug}"
);
});
}
#[test]
fn located_error_always_shows_file_line_column() {
use cutile_compiler::ast::SourceLocation;
let loc = SourceLocation::new("my/module.rs".into(), 42, 7);
let err = JITError::Located("unexpected token".into(), loc);
let output = format!("{err}");
assert_eq!(
output, "unexpected token\n --> my/module.rs:42:7",
"Located (known) variant must render message + location pointer.\n Got: {output}"
);
let outer: cutile::error::Error = err.into();
let outer_output = format!("{outer}");
assert_eq!(
outer_output, "error: unexpected token\n --> my/module.rs:42:7",
"Outer Error must prepend 'error: ' to the JITError output.\n Got: {outer_output}"
);
}
#[test]
fn untyped_literal_error_location_points_to_this_file() {
common::with_test_stack(|| {
let err = compile_and_get_error(
error_quality_untyped_literal::_module_asts(),
"error_quality_untyped_literal",
"untyped_kernel",
);
match &err {
JITError::Located(_msg, loc) => {
assert!(loc.is_known(), "Error should have a known source location");
assert!(
loc.file.ends_with("error_quality.rs"),
"Error location file should end with 'error_quality.rs', got: '{}'",
loc.file
);
let source = include_str!("error_quality.rs");
let target_line = source
.lines()
.enumerate()
.find(|(_, line)| {
let trimmed = line.trim_start();
trimmed.starts_with("let _x = 42;")
})
.map(|(idx, _)| idx + 1);
if let Some(expected_line) = target_line {
assert_eq!(
loc.line, expected_line,
"Error should point to line {expected_line} (`let _x = 42;`), \
got line {}",
loc.line
);
}
assert!(
loc.column > 0,
"Column should be non-zero for the literal, got {}",
loc.column
);
}
_ => {
println!(
"Note: error was not a Located variant: {}",
format!("{err}")
);
}
}
});
}
#[test]
fn value_verify_error_messages_are_user_facing() {
let verify_messages = [
"internal: string value has inconsistent fields set",
"internal: primitive value has inconsistent fields set",
"internal: structured type value has inconsistent fields set",
"internal: compound value has inconsistent fields set",
"internal: struct value has inconsistent fields set",
"internal: compound value missing its element list",
"internal: struct value missing its fields",
];
for msg in verify_messages {
assert_no_internal_leaks(msg, &format!("verify message: '{msg}'"));
let err = JITError::Generic(msg.to_string());
let jit_output = format!("{err}");
assert_eq!(
jit_output, msg,
"JITError::Generic must render the bare message.\n Got: {jit_output}"
);
assert_no_internal_leaks(&jit_output, &format!("formatted verify error: '{msg}'"));
let outer: cutile::error::Error = err.into();
let outer_output = format!("{outer}");
assert_single_error_prefix(&outer_output, &format!("verify error prefix: '{msg}'"));
}
}
#[test]
fn utility_error_messages_are_user_facing() {
let utility_messages = [
"failed to parse attribute `foo` with value `bar`",
"all shape dimensions must be positive, got [-1, 2]",
"type `Bogus` cannot be used as a tile type",
"unsupported element type `q16`; expected an integer (`i...`) or float (`f...`) type",
"invalid atomic mode `bogus`; valid modes are: and, or, xor, add, addf, max, min, umax, umin, xchg",
"float types only support `xchg` and `addf` atomic modes, got `And`",
"unrecognized arithmetic operation `bogus`",
"this binary operator is not supported",
"expected a variable name, got `1 + 2`",
"undefined variable `x` when updating token",
"variable `v` does not have associated type metadata (expected a view type)",
"variable `v` is missing a `token` field (expected a view with an ordering token)",
"unexpected token `@` in expression list",
];
for msg in utility_messages {
assert_no_internal_leaks(msg, &format!("utility message: '{msg}'"));
}
}
#[test]
fn literal_error_messages_are_user_facing() {
let literal_messages = [
"unable to determine type for numeric literal; add a type annotation",
"failed to compile the type of this literal",
"expected a scalar type for this literal, got a non-scalar type",
"repeat length must be a literal or const generic",
"repeat length must be an integer literal",
];
for msg in literal_messages {
assert_no_internal_leaks(msg, &format!("literal message: '{msg}'"));
}
}
#[test]
fn error_to_device_error_preserves_message() {
use cuda_async::error::DeviceError;
use cutile_compiler::ast::SourceLocation;
let jit_err = JITError::Generic("compilation failed".into());
let outer: cutile::error::Error = jit_err.into();
let device_err: DeviceError = outer.into();
let device_display = format!("{device_err}");
assert!(
device_display.contains("compilation failed"),
"DeviceError should preserve the original JIT error message.\n Got: {device_display}"
);
let loc = SourceLocation::new("k.rs".into(), 5, 3);
let jit_err = JITError::Located("type mismatch".into(), loc);
let outer: cutile::error::Error = jit_err.into();
let device_err: DeviceError = outer.into();
let device_display = format!("{device_err}");
assert!(
device_display.contains("type mismatch"),
"DeviceError should preserve the Located error message.\n Got: {device_display}"
);
assert!(
device_display.contains("k.rs"),
"DeviceError should preserve the source file from Located.\n Got: {device_display}"
);
}
#[test]
fn no_double_error_prefix_even_with_embedded_error_word() {
let err = JITError::Generic("something failed".into());
let jit_output = format!("{err}");
assert_eq!(
jit_output, "something failed",
"JITError should render bare message, got: {jit_output}"
);
let outer: cutile::error::Error = err.into();
let outer_output = format!("{outer}");
assert_single_error_prefix(&outer_output, "outer with embedded 'error' word");
}
#[test]
fn display_debug_consistency_for_all_jit_error_variants() {
use cutile_compiler::ast::SourceLocation;
let cases: Vec<(&str, JITError)> = vec![
("Generic", JITError::Generic("generic problem".into())),
(
"Located(known)",
JITError::Located(
"located problem".into(),
SourceLocation::new("f.rs".into(), 1, 0),
),
),
(
"Located(unknown)",
JITError::Located("located unknown".into(), SourceLocation::unknown()),
),
(
"Anyhow",
JITError::Anyhow(anyhow::anyhow!("anyhow problem")),
),
];
for (name, err) in &cases {
assert_display_eq_debug_jit(err, &format!("JITError::{name}"));
assert_jit_error_has_no_prefix(err, &format!("JITError::{name}"));
}
}
#[test]
fn spanned_jit_error_produces_located_variant_integration() {
use cutile_compiler::ast::SourceLocation;
use cutile_compiler::error::SpannedJITError;
let loc = SourceLocation::new("my_kernel.rs".into(), 25, 8);
let err = loc.jit_error("cannot borrow as mutable");
match &err {
JITError::Located(msg, eloc) => {
assert_eq!(msg, "cannot borrow as mutable");
assert!(eloc.is_known());
assert_eq!(eloc.file, "my_kernel.rs");
assert_eq!(eloc.line, 25);
assert_eq!(eloc.column, 8);
let output = format!("{err}");
assert_eq!(
output, "cannot borrow as mutable\n --> my_kernel.rs:25:8",
"SpannedJITError output must be bare message + location.\n Got: {output}"
);
let outer: cutile::error::Error = JITError::Located(msg.clone(), eloc.clone()).into();
let outer_output = format!("{outer}");
assert_single_error_prefix(&outer_output, "SpannedJITError → outer");
assert!(
outer_output.contains(" --> my_kernel.rs:25:8"),
"Outer Error output must include location.\n Got: {outer_output}"
);
}
other => panic!("expected Located variant, got: {other}"),
}
}
#[test]
fn compile_cuda_tile_op_error_messages_regression() {
let op_messages = [
"Expected some TypeMeta for view",
"Expected token value in TypeMeta for view",
];
for msg in op_messages {
let err = JITError::Generic(msg.to_string());
let jit_output = format!("{err}");
assert!(
!jit_output.starts_with("error: "),
"JITError must not add prefix for op message: '{msg}'.\n Got: {jit_output}"
);
let outer: cutile::error::Error = err.into();
let outer_output = format!("{outer}");
assert_single_error_prefix(&outer_output, &format!("op message outer: '{msg}'"));
}
}
#[test]
fn all_outer_error_variants_get_uniform_prefix() {
use cutile_compiler::ast::SourceLocation;
let err: cutile::error::Error = JITError::Generic("jit generic".into()).into();
assert_single_error_prefix(&format!("{err}"), "Error::JIT(Generic)");
let err: cutile::error::Error = JITError::Located(
"jit located".into(),
SourceLocation::new("f.rs".into(), 1, 0),
)
.into();
assert_single_error_prefix(&format!("{err}"), "Error::JIT(Located known)");
let err: cutile::error::Error =
JITError::Located("jit located unknown".into(), SourceLocation::unknown()).into();
assert_single_error_prefix(&format!("{err}"), "Error::JIT(Located unknown)");
let err: cutile::error::Error = JITError::Anyhow(anyhow::anyhow!("jit anyhow")).into();
assert_single_error_prefix(&format!("{err}"), "Error::JIT(Anyhow)");
let err = cutile::error::tensor_error("tensor problem");
assert_single_error_prefix(&format!("{err}"), "Error::Tensor");
let err = cutile::error::kernel_launch_error("launch problem");
assert_single_error_prefix(&format!("{err}"), "Error::KernelLaunch");
let err: cutile::error::Error = anyhow::anyhow!("anyhow problem").into();
assert_single_error_prefix(&format!("{err}"), "Error::Anyhow");
}