Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,69 @@ impl RustFloatMathIntrinsic {
"std::sys::cmath::atan2" => Some(Self::Atan2F64),
"std::sys::cmath::atanf" => Some(Self::AtanF32),
"std::sys::cmath::atan" => Some(Self::AtanF64),
other => Self::from_libm_path(other),
}
}

/// Recognize `libm` crate float functions (the `nostd-libm` lowering used
/// by glam on nvptx, e.g. `libm::math::sqrt::sqrtf`). These dependency-rlib
/// functions frequently lack exportable MIR downstream, so we intercept them
/// and lower to the same libdevice intrinsics as the core-intrinsic forms,
/// giving hardware math instead of software libm. Match on the final path
/// segment so both the canonical (`libm::math::sqrt::sqrtf`) and re-exported
/// (`libm::sqrtf`) spellings are caught.
fn from_libm_path(name: &str) -> Option<Self> {
if !is_libm_path(name) {
return None;
}
let seg = name.rsplit("::").next().unwrap_or(name);
match seg {
"sqrtf" => Some(Self::SqrtF32),
"sqrt" => Some(Self::SqrtF64),
"sinf" => Some(Self::SinF32),
"sin" => Some(Self::SinF64),
"cosf" => Some(Self::CosF32),
"cos" => Some(Self::CosF64),
"tanf" => Some(Self::TanF32),
"tan" => Some(Self::TanF64),
"expf" => Some(Self::ExpF32),
"exp" => Some(Self::ExpF64),
"exp2f" => Some(Self::Exp2F32),
"exp2" => Some(Self::Exp2F64),
"logf" => Some(Self::LogF32),
"log" => Some(Self::LogF64),
"log2f" => Some(Self::Log2F32),
"log2" => Some(Self::Log2F64),
"log10f" => Some(Self::Log10F32),
"log10" => Some(Self::Log10F64),
// libm: `powf` is the f32 power, `pow` is the f64 power.
"powf" => Some(Self::PowfF32),
"pow" => Some(Self::PowfF64),
"floorf" => Some(Self::FloorF32),
"floor" => Some(Self::FloorF64),
"ceilf" => Some(Self::CeilF32),
"ceil" => Some(Self::CeilF64),
"truncf" => Some(Self::TruncF32),
"trunc" => Some(Self::TruncF64),
"roundf" => Some(Self::RoundF32),
"round" => Some(Self::RoundF64),
// libm's `rint` documents round-half-to-even; the device has no
// dynamic rounding mode, so it shares the roundeven lowering.
"rintf" | "roundevenf" => Some(Self::RoundevenF32),
"rint" | "roundeven" => Some(Self::RoundevenF64),
"fmaf" => Some(Self::FmaF32),
"fma" => Some(Self::FmaF64),
"fabsf" | "fabs" => Some(Self::Fabs),
"copysignf" => Some(Self::CopysignF32),
"copysign" => Some(Self::CopysignF64),
"fmaxf" => Some(Self::MaxNumNszF32),
"fmax" => Some(Self::MaxNumNszF64),
"fminf" => Some(Self::MinNumNszF32),
"fmin" => Some(Self::MinNumNszF64),
"atan2f" => Some(Self::Atan2F32),
"atan2" => Some(Self::Atan2F64),
"atanf" => Some(Self::AtanF32),
"atan" => Some(Self::AtanF64),
_ => None,
}
}
Expand Down Expand Up @@ -240,6 +303,171 @@ impl RustFloatMathIntrinsic {
}
}

/// Whether `name` is a path rooted in the `libm` crate: the first path
/// segment must be exactly `libm`. A bare substring test would also match
/// user functions whose path merely mentions libm (e.g.
/// `my_app::libm_compat::expf`), silently replacing the user's body with a
/// libdevice call.
pub fn is_libm_path(name: &str) -> bool {
name.split("::").next() == Some("libm")
}

/// Recognize `libm::sincosf` / `libm::sincos` (glam's `nostd-libm` lowering of
/// `f32::sin_cos`). These return a `(sin, cos)` tuple, so they do not fit the
/// scalar `RustFloatMathIntrinsic` dispatch; [`emit_sincos`] handles them.
/// Returns `Some(is_f64)` when `name` is a libm sincos function.
pub fn libm_sincos_is_f64(name: &str) -> Option<bool> {
if !is_libm_path(name) {
return None;
}
match name.rsplit("::").next().unwrap_or(name) {
"sincosf" => Some(false),
"sincos" => Some(true),
_ => None,
}
}

/// Emit `libm::sincos{f}(x) -> (sin, cos)` as a `sinf`/`cosf` placeholder pair
/// (lowered to libdevice `__nv_sin/cosf`) packed into the destination tuple.
#[allow(clippy::too_many_arguments)]
pub fn emit_sincos(
ctx: &mut Context,
body: &mir::Body,
is_f64: bool,
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>> {
use crate::error::TranslationErr;
use crate::translator::rvalue;
use dialect_mir::ops::{MirCallOp, MirConstructTupleOp};
use pliron::builtin::attributes::StringAttr;
use pliron::input_err;
use pliron::location::Located;
use pliron::op::Op;

// Destination tuple type and its scalar element type.
let tuple_ty = types::translate_type(ctx, &body.locals()[destination.local].ty)?;
let scalar_ty = {
let r = tuple_ty.deref(ctx);
match r.downcast_ref::<dialect_mir::types::MirTupleType>() {
Some(t) => t.get_types()[0],
None => {
return input_err!(
loc.clone(),
TranslationErr::unsupported(
"libm::sincos destination is not a tuple".to_string()
)
);
}
}
};

// Translate the angle argument once and reuse it for both calls.
let (arg_value, last_op) = rvalue::translate_operand(
ctx,
body,
&args[0],
value_map,
block_ptr,
prev_op,
loc.clone(),
)?;

let (sin_callee, cos_callee) = if is_f64 {
(
rust_intrinsics::CALLEE_SIN_F64,
rust_intrinsics::CALLEE_COS_F64,
)
} else {
(
rust_intrinsics::CALLEE_SIN_F32,
rust_intrinsics::CALLEE_COS_F32,
)
};

let callee_id = pliron::identifier::Identifier::try_from("callee").unwrap();

// sin(x)
let sin_op = Operation::new(
ctx,
MirCallOp::get_concrete_op_info(),
vec![scalar_ty],
vec![arg_value],
vec![],
0,
);
sin_op.deref_mut(ctx).set_loc(loc.clone());
sin_op
.deref_mut(ctx)
.attributes
.0
.insert(callee_id.clone(), StringAttr::new(sin_callee.into()).into());
if let Some(prev) = last_op {
sin_op.insert_after(ctx, prev);
} else {
sin_op.insert_at_front(block_ptr, ctx);
}
let sin_val = sin_op.deref(ctx).get_result(0);

// cos(x)
let cos_op = Operation::new(
ctx,
MirCallOp::get_concrete_op_info(),
vec![scalar_ty],
vec![arg_value],
vec![],
0,
);
cos_op.deref_mut(ctx).set_loc(loc.clone());
cos_op
.deref_mut(ctx)
.attributes
.0
.insert(callee_id, StringAttr::new(cos_callee.into()).into());
cos_op.insert_after(ctx, sin_op);
let cos_val = cos_op.deref(ctx).get_result(0);

// Pack (sin, cos) into the destination tuple.
let tuple_op = Operation::new(
ctx,
MirConstructTupleOp::get_concrete_op_info(),
vec![tuple_ty],
vec![sin_val, cos_val],
vec![],
0,
);
tuple_op.deref_mut(ctx).set_loc(loc.clone());
tuple_op.insert_after(ctx, cos_op);
let tuple_val = tuple_op.deref(ctx).get_result(0);

let goto_prev = value_map
.store_local(ctx, destination.local, tuple_val, block_ptr, Some(tuple_op))
.unwrap_or(tuple_op);

if let Some(target_idx) = target {
Ok(helpers::emit_goto(
ctx,
*target_idx,
goto_prev,
block_map,
loc,
))
} else {
input_err!(
loc.clone(),
TranslationErr::unsupported(
"libm::sincos call without target not supported".to_string()
)
)
}
}

/// Emit a placeholder `mir.call` for a rustc float math intrinsic.
#[allow(clippy::too_many_arguments)]
pub fn emit_rust_float_math_intrinsic(
Expand Down Expand Up @@ -328,6 +556,75 @@ mod tests {
);
}

/// Libm interception must be anchored to the `libm` crate root. A user
/// function that shares a libm function name, inside a path that merely
/// mentions "libm", must stay a regular call: a bare `contains("libm")`
/// test rerouted such calls to libdevice, silently replacing the user's
/// body (miscompile caught in PR #142 review).
#[test]
fn libm_interception_is_anchored_to_the_libm_crate_root() {
// Canonical and re-exported libm spellings are intercepted.
for (path, expected) in [
("libm::math::expf::expf", RustFloatMathIntrinsic::ExpF32),
("libm::expf", RustFloatMathIntrinsic::ExpF32),
("libm::math::sqrt::sqrt", RustFloatMathIntrinsic::SqrtF64),
] {
assert_eq!(
RustFloatMathIntrinsic::from_core_path(path),
Some(expected),
"`{path}` should be intercepted as a libm function"
);
}

// Adversarial: a user `expf` under a path containing "libm" is NOT
// the libm crate and must not be rerouted.
for path in [
"my_app::libm_compat::expf",
"my_app::libm::expf",
"libmath::expf",
"libm_math::lookalike::expf",
"not_libm::expf",
] {
assert_eq!(
RustFloatMathIntrinsic::from_core_path(path),
None,
"user function `{path}` was wrongly rerouted to libdevice"
);
}
}

/// Pin the libm names that reuse existing enum variants: fmax/fmin map
/// to the `_nsz` maxNum/minNum lowering (same as `f32::max`/`f32::min`),
/// and rint/roundeven map to the round-ties-even lowering.
#[test]
fn libm_fmax_fmin_rint_roundeven_map_to_existing_variants() {
for (path, expected) in [
("libm::fmaxf", RustFloatMathIntrinsic::MaxNumNszF32),
("libm::fmax", RustFloatMathIntrinsic::MaxNumNszF64),
("libm::fminf", RustFloatMathIntrinsic::MinNumNszF32),
("libm::fmin", RustFloatMathIntrinsic::MinNumNszF64),
("libm::rintf", RustFloatMathIntrinsic::RoundevenF32),
("libm::rint", RustFloatMathIntrinsic::RoundevenF64),
("libm::roundevenf", RustFloatMathIntrinsic::RoundevenF32),
("libm::roundeven", RustFloatMathIntrinsic::RoundevenF64),
] {
assert_eq!(
RustFloatMathIntrinsic::from_core_path(path),
Some(expected),
"`{path}` did not map to the expected intrinsic"
);
}
}

/// Same anchoring requirement for the tuple-returning sincos detector.
#[test]
fn libm_sincos_detection_is_anchored_to_the_libm_crate_root() {
assert_eq!(libm_sincos_is_f64("libm::sincosf"), Some(false));
assert_eq!(libm_sincos_is_f64("libm::math::sincos::sincos"), Some(true));
assert_eq!(libm_sincos_is_f64("my_app::libm_compat::sincosf"), None);
assert_eq!(libm_sincos_is_f64("libmath::sincos"), None);
}

#[test]
fn maxnum_minnum_nsz_placeholders_round_trip_through_dialect_mir() {
// The placeholder names must match between this importer crate and
Expand Down
35 changes: 35 additions & 0 deletions crates/mir-importer/src/translator/terminator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,25 @@ fn translate_call(
);
}

// The collector skips the entire `libm` crate: every libm call must be
// intercepted by the float-math dispatch above and rerouted to a
// libdevice intrinsic, so no definition for a libm symbol ever exists in
// the module. A libm function the dispatch does not recognize would only
// fail much later, as a bare "Symbol libm__cbrtf not found" verifier
// error on the LLVM dialect module. Fail here instead, with the
// function's name and source location.
if let Some(ref name) = pattern_name
&& intrinsics::float_math::is_libm_path(name)
{
return input_err!(
loc,
TranslationErr::unsupported(format!(
"libm function `{name}` is not yet mapped to a libdevice intrinsic; \
add it to `from_libm_path` in the mir-importer float-math dispatch"
))
);
}

// Not an intrinsic - emit regular function call
let raw_name = call_name.unwrap_or_else(|| "unknown_function".to_string());
let legal_name = legaliser.legalise(&raw_name);
Expand Down Expand Up @@ -1647,6 +1666,22 @@ fn try_dispatch_intrinsic(
)?));
}

if let Some(is_f64) = intrinsics::float_math::libm_sincos_is_f64(name) {
return Ok(Some(intrinsics::float_math::emit_sincos(
ctx,
body,
is_f64,
args,
destination,
target,
block_ptr,
prev_op,
value_map,
block_map,
loc,
)?));
}

if let Some(intrinsic) = intrinsics::float_math::RustFloatMathIntrinsic::from_core_path(name) {
return Ok(Some(
intrinsics::float_math::emit_rust_float_math_intrinsic(
Expand Down
Loading
Loading