Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cuda-oxide/crates/dialect-mir/src/rust_intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ pub const CALLEE_SATURATING_SUB: &str = placeholder!("saturating_sub");
/// chunk count with `exact_div(self.len(), N)`.
pub const CALLEE_EXACT_DIV: &str = placeholder!("exact_div");

/// Placeholder call used for `core::intrinsics::float_to_int_unchecked`.
///
/// Float-to-integer conversion where the caller guarantees the value is finite
/// and fits the destination type once truncated. Backs `f32::to_int_unchecked`
/// and `f64::to_int_unchecked`.
pub const CALLEE_FLOAT_TO_INT_UNCHECKED: &str = placeholder!("float_to_int_unchecked");

/// Placeholder call used for `core::intrinsics::carrying_mul_add`.
/// Backs the bigint helper methods `carrying_mul_add`, `carrying_mul`,
/// and `widening_mul` on integer types.
Expand Down Expand Up @@ -105,6 +112,7 @@ pub fn is_known_placeholder(callee: &str) -> bool {
| CALLEE_SATURATING_ADD
| CALLEE_SATURATING_SUB
| CALLEE_EXACT_DIV
| CALLEE_FLOAT_TO_INT_UNCHECKED
| CALLEE_CARRYING_MUL_ADD
| CALLEE_SELECT_UNPREDICTABLE
)
Expand Down Expand Up @@ -308,6 +316,7 @@ mod tests {
CALLEE_SATURATING_ADD,
CALLEE_SATURATING_SUB,
CALLEE_EXACT_DIV,
CALLEE_FLOAT_TO_INT_UNCHECKED,
CALLEE_CARRYING_MUL_ADD,
CALLEE_FADD_FAST,
CALLEE_FSUB_FAST,
Expand Down
1 change: 1 addition & 0 deletions cuda-oxide/crates/mir-importer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ file:
| `debug` | Debug and profiling intrinsics |
| `exact_div` | Rust compiler `exact_div` intrinsic |
| `float_math` | Rust compiler floating-point math intrinsics |
| `float_to_int` | Rust compiler `float_to_int_unchecked` intrinsic |
| `generated` | Generated raw/compatibility path dispatch for CUDA intrinsics |
| `iket` | Translation of `cuda_device::iket` compiler markers |
| `indexing` | Thread and block indexing intrinsics |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

//! Rust compiler `float_to_int_unchecked` intrinsic.
//!
//! `core::intrinsics::float_to_int_unchecked` converts a float to an integer
//! with a caller promise: the value is not NaN or infinite, and it fits the
//! destination type once truncated toward zero. Violating that is undefined
//! behaviour, which is what lets it lower to a plain `fptosi`/`fptoui` with
//! none of the clamping and NaN handling the saturating `as` cast needs.
//!
//! `f32::to_int_unchecked` and `f64::to_int_unchecked` are the stable entry
//! points; they reach the intrinsic through `FloatToInt::to_int_unchecked`
//! (`core/src/convert/num.rs`). Without support for the intrinsic they fail to
//! translate, so device code had no unchecked float-to-integer conversion.

use super::super::helpers;
use crate::error::TranslationResult;
use crate::translator::types;
use crate::translator::values::ValueMap;
use dialect_mir::rust_intrinsics;
use pliron::basic_block::BasicBlock;
use pliron::context::{Context, Ptr};
use pliron::location::Location;
use pliron::operation::Operation;
use rustc_public::mir;

/// The `float_to_int_unchecked` intrinsic from libcore.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RustFloatToIntIntrinsic {
/// `core::intrinsics::float_to_int_unchecked`.
FloatToIntUnchecked,
}

impl RustFloatToIntIntrinsic {
/// Recognize the libcore intrinsic path that survived into MIR.
pub fn from_core_path(name: &str) -> Option<Self> {
match name {
"core::intrinsics::float_to_int_unchecked"
| "std::intrinsics::float_to_int_unchecked" => Some(Self::FloatToIntUnchecked),
_ => None,
}
}

/// Return the internal placeholder name used until MIR-to-LLVM lowering.
pub fn placeholder_callee(self) -> &'static str {
match self {
Self::FloatToIntUnchecked => rust_intrinsics::CALLEE_FLOAT_TO_INT_UNCHECKED,
}
}
}

/// Emit a placeholder `mir.call` for `core::intrinsics::float_to_int_unchecked`.
#[allow(clippy::too_many_arguments)]
pub fn emit_rust_float_to_int_intrinsic(
ctx: &mut Context,
body: &mir::Body,
intrinsic: RustFloatToIntIntrinsic,
args: &[mir::Operand],
destination: &mir::Place,
target: &Option<usize>,
block_ptr: Ptr<BasicBlock>,
prev_op: Option<Ptr<Operation>>,
value_map: &mut ValueMap,
block_map: &[Ptr<BasicBlock>],
loc: Location,
) -> TranslationResult<Ptr<Operation>> {
let return_type = types::translate_destination_type(ctx, body, destination, &loc)?;
helpers::emit_function_call(
ctx,
body,
intrinsic.placeholder_callee(),
args,
destination,
return_type,
None,
target,
block_ptr,
prev_op,
value_map,
block_map,
loc,
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn from_core_path_recognizes_float_to_int_unchecked() {
for path in [
"core::intrinsics::float_to_int_unchecked",
"std::intrinsics::float_to_int_unchecked",
] {
assert_eq!(
RustFloatToIntIntrinsic::from_core_path(path),
Some(RustFloatToIntIntrinsic::FloatToIntUnchecked),
"expected `{path}` to be recognized"
);
}
}

/// Only the intrinsic itself matches. The methods that wrap it have MIR
/// bodies of their own, which the importer translates as ordinary calls.
#[test]
fn from_core_path_rejects_neighbouring_conversion_paths() {
for path in [
"core::convert::num::FloatToInt::to_int_unchecked",
"f32::to_int_unchecked",
"core::intrinsics::float_to_int",
"core::intrinsics::exact_div",
] {
assert_eq!(
RustFloatToIntIntrinsic::from_core_path(path),
None,
"expected `{path}` not to be recognized"
);
}
}

#[test]
fn placeholder_callee_matches_the_dialect_constant() {
assert_eq!(
RustFloatToIntIntrinsic::FloatToIntUnchecked.placeholder_callee(),
rust_intrinsics::CALLEE_FLOAT_TO_INT_UNCHECKED
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
//! | `bitops` | Rust compiler bit-manipulation intrinsics |
//! | `exact_div` | Rust compiler `exact_div` |
//! | `float_math` | Rust compiler floating-point math intrinsics |
//! | `float_to_int` | Rust compiler `float_to_int_unchecked` |
//! | `saturating` | Rust compiler saturating integer intrinsics |
//!
//! # Architecture
Expand All @@ -49,6 +50,7 @@ pub mod bitops;
pub mod debug;
pub mod exact_div;
pub mod float_math;
pub mod float_to_int;
pub mod generated;
pub mod iket;
pub mod indexing;
Expand Down
19 changes: 19 additions & 0 deletions cuda-oxide/crates/mir-importer/src/translator/terminator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2881,6 +2881,25 @@ fn try_dispatch_intrinsic(
)?));
}

if let Some(intrinsic) = intrinsics::float_to_int::RustFloatToIntIntrinsic::from_core_path(name)
{
return Ok(Some(
intrinsics::float_to_int::emit_rust_float_to_int_intrinsic(
ctx,
body,
intrinsic,
args,
destination,
target,
block_ptr,
prev_op,
value_map,
block_map,
loc,
)?,
));
}

if let Some(intrinsic) = intrinsics::bigint::RustBigIntIntrinsic::from_core_path(name) {
return Ok(Some(intrinsics::bigint::emit_rust_bigint_intrinsic(
ctx,
Expand Down
112 changes: 111 additions & 1 deletion cuda-oxide/crates/mir-lower/src/convert/ops/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ use llvm_export::types::PointerTypeExt;
use pliron::builtin::attributes::IntegerAttr;
use pliron::builtin::op_interfaces::{CallOpCallable, SymbolOpInterface};
use pliron::builtin::type_interfaces::FunctionTypeInterface;
use pliron::builtin::types::{IntegerType, Signedness};
use pliron::builtin::types::{FP32Type, FP64Type, IntegerType, Signedness};
use pliron::context::{Context, Ptr};
use pliron::irbuild::dialect_conversion::{DialectConversionRewriter, OperandsInfo};
use pliron::irbuild::inserter::Inserter;
Expand Down Expand Up @@ -167,6 +167,21 @@ impl RustExactDivIntrinsic {
}
}

/// Internal placeholder for `core::intrinsics::float_to_int_unchecked`.
///
/// The caller has promised the value is finite and fits the destination type
/// once truncated, so this lowers to a plain `fptosi`/`fptoui` with none of
/// the clamping of the saturating `as` cast.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RustFloatToIntUncheckedIntrinsic;

impl RustFloatToIntUncheckedIntrinsic {
/// Convert an importer placeholder name back into the intrinsic it represents.
fn from_placeholder_callee(callee: &str) -> Option<Self> {
(callee == rust_intrinsics::CALLEE_FLOAT_TO_INT_UNCHECKED).then_some(Self)
}
}

/// Internal placeholder for rustc bigint helper intrinsics.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RustBigIntIntrinsic {
Expand Down Expand Up @@ -233,6 +248,10 @@ pub fn convert(
return convert_rust_exact_div(ctx, rewriter, op, operands_info);
}

if RustFloatToIntUncheckedIntrinsic::from_placeholder_callee(&callee_name).is_some() {
return convert_rust_float_to_int_unchecked(ctx, rewriter, op);
}

if let Some(RustBigIntIntrinsic::CarryingMulAdd) =
RustBigIntIntrinsic::from_placeholder_callee(&callee_name)
{
Expand Down Expand Up @@ -717,6 +736,73 @@ fn convert_rust_exact_div(
Ok(())
}

/// Convert `core::intrinsics::float_to_int_unchecked` to `llvm.fptosi` or
/// `llvm.fptoui`.
///
/// The intrinsic's contract is that the value is not NaN or infinite and fits
/// the destination type once truncated toward zero; violating it is undefined
/// behaviour. That is exactly the contract of a bare LLVM `fptosi`/`fptoui`,
/// so none of the clamping or NaN handling of the saturating `as` cast
/// (`llvm.fptosi.sat`/`llvm.fptoui.sat`, see `cast.rs`) is emitted.
///
/// Signedness comes from the MIR result type, the same source the saturating
/// cast uses, because the LLVM integer type is signless. Sources and
/// destinations are limited to what that cast supports on every backend:
/// `f32`/`f64` into integers of at most 64 bits.
fn convert_rust_float_to_int_unchecked(
ctx: &mut Context,
rewriter: &mut DialectConversionRewriter,
op: Ptr<Operation>,
) -> Result<()> {
let loc = op.deref(ctx).loc().clone();
if op.deref(ctx).get_num_results() != 1 {
return pliron::input_err!(loc, "float_to_int_unchecked call must have one result");
}

let args: Vec<Value> = op.deref(ctx).operands().collect();
let [value] = args[..] else {
return pliron::input_err!(loc, "float_to_int_unchecked requires one operand");
};

let is_supported_source = {
let source_ty = value.get_type(ctx);
let source_ref = source_ty.deref(ctx);
source_ref.is::<FP32Type>() || source_ref.is::<FP64Type>()
};
if !is_supported_source {
return pliron::input_err!(
loc,
"float_to_int_unchecked: only f32 and f64 sources are supported on the device"
);
}

let result_mir_ty = op.deref(ctx).get_result(0).get_type(ctx);
let (is_signed, width) = {
let result_ref = result_mir_ty.deref(ctx);
let Some(int_ty) = result_ref.downcast_ref::<IntegerType>() else {
return pliron::input_err!(loc, "float_to_int_unchecked requires an integer result");
};
// Signless lowers as unsigned, matching the saturating cast.
(int_ty.signedness() == Signedness::Signed, int_ty.width())
};
if width > 64 {
return pliron::input_err!(
loc,
"float_to_int_unchecked: integer destinations wider than 64 bits are not yet supported on the device"
);
}

let result_ty = convert_type(ctx, result_mir_ty).map_err(anyhow_to_pliron)?;
let llvm_op = if is_signed {
llvm::FPToSIOp::new(ctx, value, result_ty).get_operation()
} else {
llvm::FPToUIOp::new(ctx, value, result_ty).get_operation()
};
rewriter.insert_operation(ctx, llvm_op);
rewriter.replace_operation(ctx, op, llvm_op);
Ok(())
}

/// Lower the placeholder call for rustc's `carrying_mul_add` bigint intrinsic.
///
/// `core::intrinsics::carrying_mul_add(a, b, c, d)` computes `a * b + c + d`
Expand Down Expand Up @@ -1477,4 +1563,28 @@ mod tests {
);
}
}

/// The `float_to_int_unchecked` placeholder is recognized, and nothing
/// else is.
#[test]
fn test_float_to_int_unchecked_placeholder_round_trip() {
assert!(
RustFloatToIntUncheckedIntrinsic::from_placeholder_callee(
rust_intrinsics::CALLEE_FLOAT_TO_INT_UNCHECKED
)
.is_some()
);

for other in [
rust_intrinsics::CALLEE_EXACT_DIV,
rust_intrinsics::CALLEE_SATURATING_ADD,
rust_intrinsics::CALLEE_TRUNC_F32,
"core::intrinsics::float_to_int_unchecked",
] {
assert!(
RustFloatToIntUncheckedIntrinsic::from_placeholder_callee(other).is_none(),
"`{other}` must not be treated as float_to_int_unchecked"
);
}
}
}
Loading