Skip to content

Commit

Permalink
floats: Add fcvt instructions for doubles, and fix their overflow beh…
Browse files Browse the repository at this point in the history
…aviour to reflect the spec (#53)

This adds `fcvt.d.w[u]` and `fcvt.w[u].d` instructions, and fixes the `fcvt.s.w[u]` and `fcvt.w[u].s` instructions overflow behavior to correctly reflect the spec (Thanks Markus Böck for digging this out, as it's a bit difficult to find in the spec itself).

This also fixes a long standing bug in the way registers are stored that would introduce bugs in certain edge cases (registers are now always treated as signed values, just as the code expects in most places).
  • Loading branch information
AntonLydike authored May 14, 2024
1 parent 5735f9f commit 5e758a2
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 143 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Upcoming:

- Feature: Canonicalize register names when parsing, converting e.g. `x0 -> zero` or `fp -> s0`.
- Feature: Added support for `fcvt.d.w[u]` and `fcvt.w[u].d` instructions
- BugFix: Fixed that registers were treated as UInt32s instead of Int32 (this may have caused subtle bugs before)

## 2.2.5

Expand Down
245 changes: 115 additions & 130 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions riscemu/core/float.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import struct
import warnings
from ctypes import c_float, c_double
from typing import Union, Any, ClassVar, Type
from abc import ABC
Expand Down Expand Up @@ -130,9 +131,6 @@ def __ge__(self, other: Any):
def __bool__(self):
return bool(self.value)

def __int__(self):
return int(self.value)

def __float__(self):
return self.value

Expand Down Expand Up @@ -192,6 +190,13 @@ def __format__(self, spec: str):
return Float64.bitcast(self).__format__(spec[:-2])
return format(self.value, spec)

def __int__(self):
warnings.warn(
"Float32.__int__ is deprecated due its inability to handle infities and NaNs correctly! Use {Int32,UInt32).from_float instead!",
DeprecationWarning,
)
return int(self.value)


class Float32(BaseFloat):
_type = c_float
Expand Down
36 changes: 32 additions & 4 deletions riscemu/core/int32.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any, Union
import math

from typing import Any, Union, ClassVar
from ctypes import c_int32, c_uint32


Expand All @@ -15,6 +17,9 @@ class Int32:
_type = c_int32
__slots__ = ("_val",)

MIN_VALUE: ClassVar[int] = -(2**31)
MAX_VALUE: ClassVar[int] = 2**31 - 1

def __init__(
self, val: Union[int, c_int32, c_uint32, "Int32", bytes, bytearray, bool] = 0
):
Expand Down Expand Up @@ -205,9 +210,7 @@ def signed(self) -> "Int32":
Convert to a signed representation. See :class:Int32
:return:
"""
if self.__class__ == Int32:
return self
return Int32(self)
return self

@property
def unsigned_value(self):
Expand Down Expand Up @@ -255,6 +258,24 @@ def sign_extend(cls, data: Union[bytes, bytearray, int], bits: int):
data = (data & (2 ** (bits - 1) - 1)) - 2 ** (bits - 1)
return cls(data)

@classmethod
def from_float(cls, number: float) -> "Int32":
"""
Convert a floating point number to an instance of Int32, handling overflows and NaN according to RISC-V spec.
- Valid values and infinities are clamped between MIN_VALUE and MAX_VALUE
- NaN is treated as -inf
"""
if math.isnan(number):
number = cls.MIN_VALUE
elif number > cls.MAX_VALUE:
number = cls.MAX_VALUE
elif number < cls.MIN_VALUE:
number = cls.MIN_VALUE
else:
number = int(number)
return cls(number)


class UInt32(Int32):
"""
Expand All @@ -263,6 +284,9 @@ class UInt32(Int32):

_type = c_uint32

MIN_VALUE: ClassVar[int] = 0
MAX_VALUE: ClassVar[int] = 2**32 - 1

def unsigned(self) -> "UInt32":
"""
Return a new instance representing the same bytes, but signed
Expand All @@ -284,3 +308,7 @@ def shift_right_logical(self, amount: Union["Int32", int]) -> "UInt32":
if isinstance(amount, Int32):
amount = amount.value
return UInt32(self.value >> amount)

def signed(self) -> "Int32":
# Overflow is handled natively by the underlying c_uint32 to c_int32 conversion!
return Int32(self._val)
2 changes: 1 addition & 1 deletion riscemu/core/registers.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def set(self, reg: str, val: Int32, mark_set: bool = True) -> bool:
if not self.infinite_regs and reg not in self.valid_regs:
raise RuntimeError("Invalid register: {}".format(reg))

self.vals[reg] = val.unsigned()
self.vals[reg] = val.signed()
return True

def get(self, reg: str, mark_read: bool = True) -> Int32:
Expand Down
80 changes: 80 additions & 0 deletions riscemu/instructions/RV32D.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,83 @@
class RV32D(FloatArithBase[Float64]):
flen = 64
_float_cls = Float64

def instruction_fcvt_d_w(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11010|01 |00000|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.d.w rd,rs1
:Description:
| Converts a 32-bit signed integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd.
:Implementation:
| x[rd] = sext(s32_{f64}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float64(self.regs.get(rs).value))

def instruction_fcvt_d_wu(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11010|01 |00001|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.d.wu rd,rs1
:Description:
| Converts a 32-bit unsigned integer, in integer register rs1 into a double-precision floating-point number in floating-point register rd.
:Implementation:
| f[rd] = f64_{u32}(x[rs1])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float64(self.regs.get(rs).unsigned_value))

def instruction_fcvt_w_d(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11000|01 |00000|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.w.d rd,rs1
:Description:
| Converts a double-precision floating-point number in floating-point register rs1 to a signed 32-bit integer, in integer register rd.
:Implementation:
| x[rd] = sext(s32_{f64}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, Int32.from_float(self.regs.get_f(rs).value))

def instruction_fcvt_wu_d(self, ins: Instruction):
"""
+-----+-----+-----+-----+-----+-----+-----+---+
|31-27|26-25|24-20|19-15|14-12|11-7 |6-2 |1-0|
+-----+-----+-----+-----+-----+-----+-----+---+
|11000|01 |00001|rs1 |rm |rd |10100|11 |
+-----+-----+-----+-----+-----+-----+-----+---+
:Format:
| fcvt.wu.d rd,rs1
:Description:
| Converts a double-precision floating-point number in floating-point register rs1 to a unsigned 32-bit integer, in integer register rd.
:Implementation:
| x[rd] = sext(u32f64(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, UInt32.from_float(self.regs.get_f(rs).value))
6 changes: 3 additions & 3 deletions riscemu/instructions/RV32F.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def instruction_fcvt_w_s(self, ins: Instruction):
| x[rd] = sext(s32_{f32}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, Int32(int(self.regs.get_f(rs).value)))
self.regs.set(rd, Int32.from_float(self.regs.get_f(rs).value))

def instruction_fcvt_wu_s(self, ins: Instruction):
"""
Expand All @@ -55,7 +55,7 @@ def instruction_fcvt_wu_s(self, ins: Instruction):
| x[rd] = sext(u32_{f32}(f[rs1]))
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set(rd, UInt32(int(self.regs.get_f(rs).value)))
self.regs.set(rd, UInt32.from_float((self.regs.get_f(rs).value)))

def instruction_fmv_x_w(self, ins: Instruction):
"""
Expand Down Expand Up @@ -95,7 +95,7 @@ def instruction_fcvt_s_w(self, ins: Instruction):
| f[rd] = f32_{s32}(x[rs1])
"""
rd, rs = self.parse_rd_rs(ins)
self.regs.set_f(rd, Float32(self.regs.get(rs).signed().value))
self.regs.set_f(rd, Float32(self.regs.get(rs).value))

def instruction_fcvt_s_wu(self, ins: Instruction):
"""
Expand Down
4 changes: 2 additions & 2 deletions test/filecheck/rv32f-conv.asm
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ main:
fcvt.s.w fa0, a1
fmv.x.w a1, fa0
print a1
// CHECK-NEXT: register a1 contains value 3221225472
// CHECK-NEXT: register a1 contains value -1073741824

// test fmv.w.x
li a1, 1073741824
fmv.w.x fa0, a1
print.float.s fa0
// CHECK-NEXT: register fa0 contains value 2.0
li a1, 3221225472
li a1, -1073741824
fmv.w.x fa0, a1
print.float.s fa0
// CHECK-NEXT: register fa0 contains value -2.0
Expand Down
39 changes: 39 additions & 0 deletions test/test_integers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from riscemu.core import Int32, UInt32
import pytest


def test_logical_right_shift():
Expand All @@ -13,3 +14,41 @@ def test_logical_right_shift():
assert a.shift_right_logical(10) == 4194303
assert a.shift_right_logical(31) == 1
assert a.shift_right_logical(32) == 0


@pytest.mark.parametrize(
"val,expected_int",
(
(
(0.0, 0),
(-1.0, -1),
(3.14159, 3),
(float("NaN"), Int32.MIN_VALUE),
(float("-inf"), Int32.MIN_VALUE),
(float("inf"), Int32.MAX_VALUE),
(1.0e100, Int32.MAX_VALUE),
(-1.0e100, Int32.MIN_VALUE),
)
),
)
def test_float_to_int_conversion(val: float, expected_int: int):
assert Int32.from_float(val) == expected_int


@pytest.mark.parametrize(
"val,expected_int",
(
(
(0.0, 0),
(3.14159, 3),
(float("NaN"), UInt32.MIN_VALUE),
(float("-inf"), UInt32.MIN_VALUE),
(float("inf"), UInt32.MAX_VALUE),
(1.0e100, UInt32.MAX_VALUE),
(-1.0e100, UInt32.MIN_VALUE),
(-1.0, UInt32.MIN_VALUE),
)
),
)
def test_float_to_uint_conversion(val: float, expected_int: int):
assert UInt32.from_float(val) == expected_int

0 comments on commit 5e758a2

Please sign in to comment.