Repository navigation
# Missing Code Transformation: IR.ConvertU64ToR8 (and ConvertI64ToR8 silently truncates on x86) #1297
Description
Description
Compiling code that converts a ulong to double fails with a "Missing Code Transformation" build error on the BareMetal x86 target. Investigating this surfaced a second, more concerning issue: the signed equivalent (long to double), which does compile on x86, silently produces incorrect results for values above roughly 2^31.
This was found while implementing a system tick counter for a hobby OS project built on MOSA BareMetal (x86). The counter is a ulong, and dividing it (as double) to compute elapsed seconds triggered the build failure below.
Version tested
Mosa.Platform/Mosa.Platform.x86/Mosa.DeviceSystem/Mosa.Tools.Package 2.6.1.1669. Confirmed via source inspection that the relevant transforms are unchanged as of the current master (checked against a commit from May 2026), so this is not version-specific to 1669.
Environment
- Visual Studio 2026 Community, v18.9.2
- .NET SDK 10.0.400
- OS: Windows 10.0.26200 (win-x64)
- Target: BareMetal x86, tested in QEMU (
qemu-system-i386) - Project targets
net10.0,LangVersion 14.0
Problem 1 — IR.ConvertU64ToR8 has no real implementation on any platform
Steps to reproduce
Compile a method that converts a ulong to double, e.g.:
public static double ToSeconds(ulong ticks, uint frequencyHz)
{
return (double)ticks / frequencyHz;
}Actual behavior
Error => Missing Code Transformation: 00020: IR.ConvertU64ToR8 XMM#0 [R8] <= EAX [I8] in <MethodName>
Expected behavior
Either the conversion compiles and produces a correct result, or a clear, documented limitation is surfaced earlier (e.g. at the C# level) rather than a low-level IR transform error.
Root cause
IR.ConvertU64ToR8 has no real code transformation implemented on any platform I could find (x86, x64, ARM32). The only related file is a constant-folding optimization (Source/Mosa.Compiler.Framework/Transforms/Optimizations/Auto/ConstantFolding/ConvertU64ToR8.cs), which only helps when the value is a compile-time constant — it doesn't help for runtime values, which is the overwhelmingly common case.
Problem 2 — IR.ConvertI64ToR8 compiles on x86 but silently truncates
While looking for a workaround (casting to long first, since it's signed), I found that the signed conversion, while it does compile on x86, is itself incomplete.
Root cause
Source/Mosa.Compiler.x86/Transforms/BaseIR/ConvertI64ToR8.cs:
public override void Transform(Context context, Transform transform)
{
transform.SplitOperand(context.Operand1, out var op1Low, out _);
context.SetInstruction(X86.Cvtsi2sd32, context.Result, op1Low);
}This splits the 64-bit operand into low/high 32-bit halves and discards the high half, converting only the low 32 bits via Cvtsi2sd32. For any value whose magnitude exceeds ~2^31, this silently produces a wrong result — no exception, no crash, just an incorrect double.
For comparison, the x64 version (Source/Mosa.Compiler.x64/Transforms/BaseIR/ConvertI64ToR8.cs) is correct — it uses Cvtsi2sd64 with the full 64-bit register:
public override void Transform(Context context, Transform transform)
{
context.SetInstruction(X64.Cvtsi2sd64, context.Result, context.Operand1);
}So this is x86-32-bit-specific. By contrast, ConvertU32ToR8 (uint to double) is correctly implemented on x86 (Source/Mosa.Compiler.x86/Transforms/BaseIR/ConvertU32ToR8.cs) — it uses the full 32-bit value directly via Cvtsi2sd32, nothing is discarded, since the source is already 32-bit.
Why this is worse than Problem 1
Problem 1 is a hard compile error — annoying, but safe (you find out immediately). Problem 2 is silent: code compiles, appears to work in early testing (small values), and only produces wrong output once a counter or accumulator crosses ~2^31 — e.g. after about 25 days of uptime for a tick counter running at 1kHz. This seems like the more dangerous of the two.
Historical context
I found a MOSA Discord conversation from April 2022 (#compiler channel) where charsleysa and tgiphil solved essentially the same class of problem, but in the opposite direction (double to int64, i.e. ConvertR8ToI64/ConvertR8ToU64). They discussed and rejected the legacy x87 FPU (FLD/FISTTP), confirmed CVTTSD2SI doesn't support a 64-bit destination in 32-bit x86 mode, and ultimately implemented a pure-software solution: manually decomposing/reconstructing the IEEE 754 sign/exponent/mantissa bits, avoiding both the FPU and SSE entirely. That solution is present in current master as Mosa.Runtime.Math.Conversion.R8ToI8/R8ToU8/etc. (Source/Mosa.Runtime/Math/Conversion.cs), invoked via RuntimeCall-category transforms.
Notably, charsleysa wrote at the time: "though I'll probably also add the reverse conversion too" — explicitly acknowledging that the reverse direction (int-to-double, i.e. this issue) would also be needed. As far as I can tell from the current source, that follow-up was never added — there's no I8ToR8/U8ToR8/I4ToR8/U4ToR8 anywhere in Conversion.cs today. I wasn't able to confirm from GitHub search alone whether a PR was ever opened and abandoned, or if it just fell off the list — flagging in case it rings a bell for anyone who was around at the time.
Suggested approach
Given the precedent above, the same bit-decomposition technique — run in reverse (constructing a double's sign/exponent/mantissa from an integer instead of extracting them) — seems like a natural fit, consistent with how R8ToI8/etc. avoid the FPU/SSE limitations on x86 entirely.
Workaround in use
For anyone hitting this: avoid converting a large-magnitude 64-bit integer to double directly. Split the value using integer arithmetic first (e.g. divide into a "whole units" part and a small remainder), and only convert the resulting smaller-magnitude values to double. This sidesteps both problems as long as the "whole units" part stays under ~2^31 in your use case.
Additional notes
- No similar open issue found in the tracker as of this report.
- Happy to test a candidate fix against a real bare-metal repro if useful.
Activity