| 1 | //! Pure leaf helpers used throughout lowering. |
| 2 | //! |
| 3 | //! Type coercion, integer widening, and storage-size queries — small |
| 4 | //! self-contained utilities that don't carry any `LowerCtx` state. |
| 5 | //! |
| 6 | //! Extracted from `lower::core` in sprint 04 step 4. No behavior |
| 7 | //! change. |
| 8 | |
| 9 | use crate::ir::builder::FuncBuilder; |
| 10 | use crate::ir::inst::{CmpOp, ValueId}; |
| 11 | use crate::ir::types::{FloatWidth, IntWidth, IrType}; |
| 12 | |
| 13 | /// Coerce a scalar value to a target type for initializer storage. |
| 14 | /// |
| 15 | /// Covers every Fortran scalar coercion that can show up at an |
| 16 | /// initializer-store site: |
| 17 | /// * Int → Int width change (sign-extend or truncate). Audit |
| 18 | /// Min-3: Fortran integers are always signed, so the int_extend |
| 19 | /// `signed` flag is hardcoded to `true`. |
| 20 | /// * Int ↔ Float (round to nearest for Float→Int). |
| 21 | /// * F32 ↔ F64 (extend / truncate). |
| 22 | /// * Bool ↔ Int (round-trip via int_extend; Fortran logicals |
| 23 | /// occupy a full kind so this is rare but legal). |
| 24 | /// |
| 25 | /// Anything that doesn't match one of those cases falls into the |
| 26 | /// `_ => val` arm and a `debug_assert!` fires — silently passing |
| 27 | /// the wrong-typed value would let a future caller wire mismatched |
| 28 | /// types into a Store, which the verifier (after MAJOR-4) would |
| 29 | /// then catch much later. Better to fail loudly at the source. |
| 30 | pub(super) fn coerce_to_type(b: &mut FuncBuilder, val: ValueId, target: &IrType) -> ValueId { |
| 31 | let src = match b.func().value_type(val) { |
| 32 | Some(t) => t, |
| 33 | None => return val, |
| 34 | }; |
| 35 | if src == *target { |
| 36 | return val; |
| 37 | } |
| 38 | match (&src, target) { |
| 39 | // Complex values commonly travel as ptr<[f32/f64 x 2]> buffers. |
| 40 | // When a by-value complex slot expects the aggregate itself, |
| 41 | // materialize it by loading the pointed-to pair. |
| 42 | (IrType::Ptr(inner), target) |
| 43 | if matches!(inner.as_ref(), IrType::Array(_, 2)) && inner.as_ref() == target => |
| 44 | { |
| 45 | b.load_typed(val, target.clone()) |
| 46 | } |
| 47 | // Complex → real coercion (F2018 §10.2.1.3): assigning a |
| 48 | // complex value to a real variable extracts its real |
| 49 | // component. Common in BLAS code like `rtemp = rtemp + |
| 50 | // conjg(z)*z` where the RHS is mathematically real but |
| 51 | // typed complex. |
| 52 | (IrType::Ptr(inner), IrType::Float(target_fw)) |
| 53 | if matches!(inner.as_ref(), |
| 54 | IrType::Array(elem, 2) if matches!(elem.as_ref(), IrType::Float(_))) => |
| 55 | { |
| 56 | let elem_fw = match inner.as_ref() { |
| 57 | IrType::Array(elem, _) => match elem.as_ref() { |
| 58 | IrType::Float(fw) => *fw, |
| 59 | _ => unreachable!(), |
| 60 | }, |
| 61 | _ => unreachable!(), |
| 62 | }; |
| 63 | let zero = b.const_i64(0); |
| 64 | let re_ptr = b.gep(val, vec![zero], IrType::Int(IntWidth::I8)); |
| 65 | let re = b.load_typed(re_ptr, IrType::Float(elem_fw)); |
| 66 | // Width adjust if target precision differs from element. |
| 67 | if elem_fw == *target_fw { |
| 68 | re |
| 69 | } else if elem_fw == FloatWidth::F32 && *target_fw == FloatWidth::F64 { |
| 70 | b.float_extend(re, FloatWidth::F64) |
| 71 | } else { |
| 72 | b.float_trunc(re, *target_fw) |
| 73 | } |
| 74 | } |
| 75 | // Int → Complex (F2018 §10.1.10.1, F2018 §16.9.43): cmplx(i) |
| 76 | // semantics — integer becomes the real part, imaginary part 0. |
| 77 | // Common via `zero_value_for_ir_type` for missing optional |
| 78 | // complex args returning a scalar zero, or coerce_value_call_arg |
| 79 | // when an integer literal flows into a complex VALUE parameter. |
| 80 | (IrType::Int(_), IrType::Array(elem, 2)) if matches!(elem.as_ref(), IrType::Float(_)) => { |
| 81 | let fw = match elem.as_ref() { |
| 82 | IrType::Float(fw) => *fw, |
| 83 | _ => unreachable!(), |
| 84 | }; |
| 85 | let re = b.int_to_float(val, fw); |
| 86 | let zero = match fw { |
| 87 | FloatWidth::F32 => b.const_f32(0.0), |
| 88 | FloatWidth::F64 => b.const_f64(0.0), |
| 89 | }; |
| 90 | let buf = b.alloca(IrType::Array(Box::new(IrType::Float(fw)), 2)); |
| 91 | let zero_off = b.const_i64(0); |
| 92 | let lane_bytes = |
| 93 | b.const_i64(if fw == FloatWidth::F64 { 8 } else { 4 }); |
| 94 | let re_ptr = b.gep(buf, vec![zero_off], IrType::Int(IntWidth::I8)); |
| 95 | let im_ptr = b.gep(buf, vec![lane_bytes], IrType::Int(IntWidth::I8)); |
| 96 | b.store(re, re_ptr); |
| 97 | b.store(zero, im_ptr); |
| 98 | b.load_typed(buf, IrType::Array(Box::new(IrType::Float(fw)), 2)) |
| 99 | } |
| 100 | // Real → Complex (F2018 §10.1.10.1): real becomes real part, |
| 101 | // imaginary part 0. Width-adjusts when source and target |
| 102 | // float widths differ (e.g. real(sp) literal → complex(dp)). |
| 103 | (IrType::Float(src_fw), IrType::Array(elem, 2)) if matches!(elem.as_ref(), IrType::Float(_)) => { |
| 104 | let target_fw = match elem.as_ref() { |
| 105 | IrType::Float(fw) => *fw, |
| 106 | _ => unreachable!(), |
| 107 | }; |
| 108 | let re = if *src_fw == target_fw { |
| 109 | val |
| 110 | } else if *src_fw == FloatWidth::F32 && target_fw == FloatWidth::F64 { |
| 111 | b.float_extend(val, FloatWidth::F64) |
| 112 | } else { |
| 113 | b.float_trunc(val, target_fw) |
| 114 | }; |
| 115 | let zero = match target_fw { |
| 116 | FloatWidth::F32 => b.const_f32(0.0), |
| 117 | FloatWidth::F64 => b.const_f64(0.0), |
| 118 | }; |
| 119 | let buf = b.alloca(IrType::Array(Box::new(IrType::Float(target_fw)), 2)); |
| 120 | let zero_off = b.const_i64(0); |
| 121 | let lane_bytes = |
| 122 | b.const_i64(if target_fw == FloatWidth::F64 { 8 } else { 4 }); |
| 123 | let re_ptr = b.gep(buf, vec![zero_off], IrType::Int(IntWidth::I8)); |
| 124 | let im_ptr = b.gep(buf, vec![lane_bytes], IrType::Int(IntWidth::I8)); |
| 125 | b.store(re, re_ptr); |
| 126 | b.store(zero, im_ptr); |
| 127 | b.load_typed(buf, IrType::Array(Box::new(IrType::Float(target_fw)), 2)) |
| 128 | } |
| 129 | // Int → Float |
| 130 | (IrType::Int(_), IrType::Float(fw)) => b.int_to_float(val, *fw), |
| 131 | // Float → Int |
| 132 | (IrType::Float(_), IrType::Int(iw)) => b.float_to_int(val, *iw), |
| 133 | // F32 ↔ F64 |
| 134 | (IrType::Float(FloatWidth::F32), IrType::Float(FloatWidth::F64)) => { |
| 135 | b.float_extend(val, FloatWidth::F64) |
| 136 | } |
| 137 | (IrType::Float(FloatWidth::F64), IrType::Float(FloatWidth::F32)) => { |
| 138 | b.float_trunc(val, FloatWidth::F32) |
| 139 | } |
| 140 | // Int width change. Audit Min-3: Fortran integers are signed. |
| 141 | (IrType::Int(src_w), IrType::Int(dst_w)) => { |
| 142 | if dst_w.bits() > src_w.bits() { |
| 143 | b.int_extend(val, *dst_w, true) |
| 144 | } else if dst_w.bits() < src_w.bits() { |
| 145 | b.int_trunc(val, *dst_w) |
| 146 | } else { |
| 147 | val |
| 148 | } |
| 149 | } |
| 150 | // Bool ↔ Int via int_extend. Bool is i1 in our model. |
| 151 | (IrType::Bool, IrType::Int(iw)) => b.int_extend(val, *iw, false), |
| 152 | // Int → Bool: compare against zero to produce a true Bool |
| 153 | // rather than truncating to i8 (which the verifier would |
| 154 | // then reject on any .and./.or. operand). Common path: |
| 155 | // LOGICAL fields in derived types load as i8 and need to |
| 156 | // reach Bool before a logical op (audit31 Finding 13). |
| 157 | (IrType::Int(_), IrType::Bool) => { |
| 158 | let zero = match &src { |
| 159 | IrType::Int(IntWidth::I64) => b.const_i64(0), |
| 160 | IrType::Int(IntWidth::I16) => b.const_i32(0), |
| 161 | IrType::Int(IntWidth::I8) => b.const_i32(0), |
| 162 | _ => b.const_i32(0), |
| 163 | }; |
| 164 | // Widen to i32 first if the source is narrower so |
| 165 | // icmp gets matching operand widths. |
| 166 | let widened = match &src { |
| 167 | IrType::Int(IntWidth::I8) | IrType::Int(IntWidth::I16) => { |
| 168 | b.int_extend(val, IntWidth::I32, false) |
| 169 | } |
| 170 | _ => val, |
| 171 | }; |
| 172 | b.icmp(CmpOp::Ne, widened, zero) |
| 173 | } |
| 174 | // Ptr<Array<T, N>> → Ptr<T>: pointer to array used as pointer to element. |
| 175 | // Common for character arrays (Ptr<[i8 x 20]> → Ptr<i8>). |
| 176 | (IrType::Ptr(_), IrType::Ptr(_)) => { |
| 177 | // Pointers are all the same size on ARM64 — pass through. |
| 178 | val |
| 179 | } |
| 180 | // Int → Ptr: value used in pointer context (e.g., byte as char*). |
| 181 | (IrType::Int(_), IrType::Ptr(_)) => b.int_to_ptr(val, IrType::Int(IntWidth::I8)), |
| 182 | // Ptr → Int: pointer used in integer context. |
| 183 | (IrType::Ptr(_), IrType::Int(IntWidth::I64)) => b.ptr_to_int(val), |
| 184 | (IrType::Ptr(_), IrType::Int(iw)) => { |
| 185 | let i64_val = b.ptr_to_int(val); |
| 186 | b.int_trunc(i64_val, *iw) |
| 187 | } |
| 188 | // Ptr<derived/byte-aggregate> → Float: nothing to do at the |
| 189 | // IR level. This arises when generic dispatch picks a |
| 190 | // wrong-typed specific (e.g. a structure constructor for a |
| 191 | // type that has a same-named generic interface). Returning |
| 192 | // the val unchanged would propagate a struct ptr to a store |
| 193 | // that expects a float and trip the IR verifier; emit a |
| 194 | // typed zero of the target so the call/store stays well-typed |
| 195 | // (the call's runtime semantics are already broken — this |
| 196 | // just keeps the verifier from rejecting the surrounding IR). |
| 197 | (IrType::Ptr(_), IrType::Float(fw)) => match fw { |
| 198 | FloatWidth::F32 => b.const_f32(0.0), |
| 199 | FloatWidth::F64 => b.const_f64(0.0), |
| 200 | }, |
| 201 | // Ptr<Bool> → Bool: dereference the pointer. Stdlib's masked |
| 202 | // reductions (`stdlib_sum_1d_sp_mask` etc.) hit this when the |
| 203 | // mask actual is passed by reference but the inner expression |
| 204 | // expects a bool value. A typed load keeps the IR consistent |
| 205 | // and the runtime correct. |
| 206 | (IrType::Ptr(inner), IrType::Bool) if matches!(**inner, IrType::Bool) => { |
| 207 | b.load_typed(val, IrType::Bool) |
| 208 | } |
| 209 | // Ptr<i8> → Bool: load the byte and treat it as a logical. |
| 210 | // Fortran logical(1) flows through the IR as a pointer to i8 |
| 211 | // (the in-memory representation), but element-wise intrinsic |
| 212 | // bodies (merge/where) expect the element value as a Bool. |
| 213 | // Without this, merge() with a logical(1) mask array failed |
| 214 | // to compile and crashed the IR verifier. |
| 215 | (IrType::Ptr(inner), IrType::Bool) |
| 216 | if matches!(**inner, IrType::Int(IntWidth::I8)) => |
| 217 | { |
| 218 | let byte = b.load_typed(val, IrType::Int(IntWidth::I8)); |
| 219 | let zero = b.const_int(0, IntWidth::I8); |
| 220 | b.icmp(CmpOp::Ne, byte, zero) |
| 221 | } |
| 222 | _ => { |
| 223 | eprintln!( |
| 224 | "coerce_to_type: unhandled coercion {:?} → {:?}", |
| 225 | src, target |
| 226 | ); |
| 227 | val |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | pub(super) fn widen_to_i64(b: &mut FuncBuilder, value: ValueId) -> ValueId { |
| 233 | match b.func().value_type(value) { |
| 234 | Some(IrType::Int(IntWidth::I64)) => value, |
| 235 | _ => coerce_to_type(b, value, &IrType::Int(IntWidth::I64)), |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | pub(super) fn clamp_nonnegative_i64(b: &mut FuncBuilder, value: ValueId) -> ValueId { |
| 240 | let widened = widen_to_i64(b, value); |
| 241 | let zero = b.const_i64(0); |
| 242 | let is_nonnegative = b.icmp(CmpOp::Ge, widened, zero); |
| 243 | b.select(is_nonnegative, widened, zero) |
| 244 | } |
| 245 | |
| 246 | pub(super) fn const_range_for_ir_type(ty: &IrType) -> Option<i128> { |
| 247 | Some(match ty { |
| 248 | IrType::Int(IntWidth::I8) => 2, |
| 249 | IrType::Int(IntWidth::I16) => 4, |
| 250 | IrType::Int(IntWidth::I32) => 9, |
| 251 | IrType::Int(IntWidth::I64) => 18, |
| 252 | IrType::Int(IntWidth::I128) => 38, |
| 253 | IrType::Float(FloatWidth::F32) => 37, |
| 254 | IrType::Float(FloatWidth::F64) => 307, |
| 255 | _ => return None, |
| 256 | }) |
| 257 | } |
| 258 | |
| 259 | /// Storage size in bits (F2018 §16.9.196 STORAGE_SIZE) for a value of |
| 260 | /// the given IR type. Walks pointer/array wrappers so callers passing |
| 261 | /// the *address* of a value still get the value's storage size. |
| 262 | pub(super) fn storage_size_bits_for_ir_type(ty: &IrType) -> i32 { |
| 263 | match ty { |
| 264 | IrType::Int(IntWidth::I8) => 8, |
| 265 | IrType::Int(IntWidth::I16) => 16, |
| 266 | IrType::Int(IntWidth::I32) => 32, |
| 267 | IrType::Int(IntWidth::I64) => 64, |
| 268 | IrType::Int(IntWidth::I128) => 128, |
| 269 | IrType::Float(FloatWidth::F32) => 32, |
| 270 | IrType::Float(FloatWidth::F64) => 64, |
| 271 | IrType::Bool => 32, |
| 272 | IrType::Array(elem, n) => { |
| 273 | let elem_bits = storage_size_bits_for_ir_type(elem); |
| 274 | elem_bits.saturating_mul(*n as i32) |
| 275 | } |
| 276 | // Pointer to a value — return the storage size of the pointee |
| 277 | // (this is the form taken by descriptor-passed actuals). |
| 278 | IrType::Ptr(inner) => storage_size_bits_for_ir_type(inner), |
| 279 | _ => 0, |
| 280 | } |
| 281 | } |
| 282 |