-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFpu.py
More file actions
2065 lines (1490 loc) · 95 KB
/
Fpu.py
File metadata and controls
2065 lines (1490 loc) · 95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import math
import struct
from typing import override
from Custom_errors import FpuError
class FPU_multiplier_divider:
@override
def __init__(self) -> None:
self.external_methods: list[str] = ["multiply_floats", "divide_floats"]
@override
def __str__(self) -> str:
return f"< The multiplier and divider subunit of the FPU with the external methods: {self.external_methods} >\n< Takes int and or float as an input and returns a float type. >"
@override
def __repr__(self) -> str:
return self.__str__()
#Internal methods
#NOTE: Floating point multiplication and division shared internal methods start here
def binary_to_float(self, fpn_bit_string: str, bit_len: int = 64) -> float:
"""
Convert IEEE 754 binary representation back to a floating point number.
Handles all IEEE 754 cases including:
- Normal numbers with implicit leading 1
- Subnormal numbers (denormalized)
- Special values: +/-0, +/-infinity, NaN
Args:
fpn_bit_string: Binary string representation of the float
bit_len: Bit length (32 for single precision, 64 for double precision)
Returns:
Reconstructed floating point number
Raises:
ValueError: If bit_len is not 32 or 64
"""
sing_bit_string: str = ""
exponent_string: str = ""
mantissa_string: str = ""
output_num: float = 0
sing_bit_string = fpn_bit_string[0]
if bit_len == 32:
exponent_string = fpn_bit_string[1:9]
mantissa_string = fpn_bit_string[9:len(fpn_bit_string)]
exp_offset: int = 127 #ensures that one can have fractional numbers by shifting the exponent into negative. Roughly half of the range is the bias/offset.
elif bit_len == 64:
exponent_string = fpn_bit_string[1:12]
mantissa_string = fpn_bit_string[12:len(fpn_bit_string)]
exp_offset: int = 1023
else:
raise ValueError(f"binary_to_float: Unsupported bit length, 32 or 64 expected, {bit_len} given.")
sing_bit: int = int(sing_bit_string)
mantissa: float = float()
for bit_index, bit in enumerate(mantissa_string):
mantissa += int(bit) * (2 ** (-bit_index - 1))
exponent: int = int('0b' + exponent_string, 2)
#Calculations with special cases
if exponent == 0 and mantissa == 0: #the case of a 0
output_num = 0.0
elif exponent == 0 and mantissa != 0: #the case of very small numbers (subnormal numbers)
output_num = (-1) ** sing_bit * (0 + mantissa) * (2 ** (1 - exp_offset))
elif all(bit == '1' for bit in exponent_string) and mantissa == 0: #the case of a too large number = overflows
if sing_bit == 0:
return float('Inf')
elif sing_bit == 1:
return float('-Inf')
elif all(bit == '1' for bit in exponent_string) and mantissa != 0: #the case of results of invalid or undefined mathematical operations
return float('NaN')
else: # Normal case
output_num = (-1) ** sing_bit * (1 + mantissa) * (2 ** (exponent - exp_offset))
return output_num
def full_adder(self, input_a: int = 0, input_b: int = 0, carry_in: int = 0) -> tuple[int, int]:
"""
Hardware-level 1-bit full adder implementation.
Performs binary addition of two single bits plus a carry input,
producing a sum bit and carry output. This mimics actual CPU
arithmetic logic unit (ALU) behavior for bit-accurate arithmetic.
The implementation uses XOR for sum calculation and combines AND/OR
logic for carry propagation, matching standard digital logic design.
Args:
input_a: First input bit (0 or 1, defaults to 0)
input_b: Second input bit (0 or 1, defaults to 0)
carry_in: Carry input from previous bit position (0 or 1, defaults to 0)
Returns:
tuple[int, int]: (sum_bit, carry_out) where:
- sum_bit: Result of XOR operation on all three inputs
- carry_out: 1 if two or more inputs are 1, otherwise 0
Note:
All inputs are expected to be 0 or 1. The function implements
the standard truth table for binary addition at the bit level.
"""
carry: int = carry_in
carry_out: int = 0
bit_sum: int = (input_a ^ input_b) ^ carry
if input_a & input_b:
carry_out = 1
elif (input_a | input_b) & carry:
carry_out = 1
else:
carry_out = 0
return bit_sum, carry_out
def float_rounder(self, exponent: str, mantissa: str, rounding_bits: str) -> tuple[str, str]:
"""
IEEE 754 compliant rounding using Guard, Round, and Sticky bits.
Implements "round to nearest, ties to even" (banker's rounding) with
hardware-accurate bit manipulation. The function handles mantissa overflow
by propagating carries through the exponent using a full adder chain.
Rounding Algorithm:
- If guard bit = 0: truncate (no rounding)
- If guard bit = 1:
- If round = 0 and sticky = 0: round to even (check LSB of mantissa)
- If round = 1 or sticky = 1: always round up
Implementation Details:
- Reverses bit arrays for LSB-first processing during addition
- Uses sticky bit calculation via OR reduction of all trailing bits
- Employs hardware-level full adder for carry propagation
- Handles mantissa overflow by incrementing exponent
- Maintains bit-accurate arithmetic throughout the process
Args:
exponent: Binary string of the biased exponent (any length)
mantissa: Binary string of the mantissa without implicit leading bit
rounding_bits: Extra precision bits in format "GRS..." where:
- G: Guard bit (position 0)
- R: Round bit (position 1)
- S...: Sticky bits (positions 2+, OR'd together)
Returns:
tuple[str, str]: (rounded_exponent, rounded_mantissa)
- If no rounding needed: returns original values
- If rounding causes overflow: returns incremented exponent
- If invalid state: returns ('', '') as error indicator
Note:
The function modifies bit arrays in-place using reversed order for
efficient carry propagation, then reverses back for output format.
Mantissa overflow automatically triggers exponent increment.
"""
mantissa_lst: list[int] = [int(bit) for bit in mantissa]
mantissa_lst.reverse() #reversed for bitwise operations!
exponent_lst: list[int] = [int(bit) for bit in exponent]
exponent_lst.reverse() #reversed for bitwise operations!
sticky_bits_lst: list[int] = [int(bit) for bit in rounding_bits[2:]]
guard_bit: int = int(rounding_bits[0])
rounding_bit: int = int(rounding_bits[1])
sticky_bit: int = 0
for bit in sticky_bits_lst:
sticky_bit = sticky_bit | int(bit)
carry_over: int = 0
rounded_mantissa: list[int] = []
rounded_exponent: list[int] = []
rounded_exponent_out: str = ""
rounded_mantissa_out: str = ""
if guard_bit == 0: #guard bit (g)
return exponent, mantissa
elif guard_bit == 1 and rounding_bit == 0 and sticky_bit == 0:
if mantissa_lst[0] == 0:
return exponent, mantissa
else:
for bit_index, bit in enumerate(mantissa_lst):
new_bit: int = 0
if bit_index == 0:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = 1)
rounded_mantissa.append(new_bit)
else:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = carry_over)
rounded_mantissa.append(new_bit)
if carry_over == 0:
rounded_mantissa.reverse()
rounded_mantissa_out = ''.join(str(bit) for bit in rounded_mantissa)
return exponent, rounded_mantissa_out
else:
for bit_index, bit in enumerate(exponent_lst):
new_bit: int = 0
if bit_index == 0:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = 1)
rounded_exponent.append(new_bit)
else:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = carry_over)
rounded_exponent.append(new_bit)
rounded_exponent.reverse()
rounded_mantissa.reverse()
rounded_exponent_out = ''.join(str(bit) for bit in rounded_exponent)
rounded_mantissa_out = ''.join(str(bit) for bit in rounded_mantissa)
return rounded_exponent_out, rounded_mantissa_out
elif guard_bit == 1 and (rounding_bit == 1 or sticky_bit == 1):
for bit_index, bit in enumerate(mantissa_lst):
new_bit: int = 0
if bit_index == 0:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = 1)
rounded_mantissa.append(new_bit)
else:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = carry_over)
rounded_mantissa.append(new_bit)
if carry_over == 0:
rounded_mantissa.reverse()
rounded_mantissa_out = ''.join(str(bit) for bit in rounded_mantissa)
return exponent, rounded_mantissa_out
else:
for bit_index, bit in enumerate(exponent_lst):
new_bit: int = 0
if bit_index == 0:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = 1)
rounded_exponent.append(new_bit)
else:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = carry_over)
rounded_exponent.append(new_bit)
rounded_exponent.reverse()
rounded_mantissa.reverse()
rounded_exponent_out = ''.join(str(bit) for bit in rounded_exponent)
rounded_mantissa_out = ''.join(str(bit) for bit in rounded_mantissa)
return rounded_exponent_out, rounded_mantissa_out
else:
return '', ''
def float_to_binary(self, num: float | int, bit_len: int = 64) -> str:
"""
Convert a floating point number to binary representation in IEEE 754 format.
Implements complete IEEE 754 standard with hardware-accurate precision and
rounding. Handles the full spectrum of IEEE 754 values including normal,
subnormal, zero, infinity, and NaN cases with proper bit-level accuracy.
IEEE 754 Support:
- Normal numbers: Standard 1.mantissa × 2^(exponent-bias) format
- Subnormal numbers: 0.mantissa × 2^(1-bias) format for values near zero
- Special values: +/-0, +/-infinity, NaN with correct bit patterns
- Precision modes: Single (32-bit) and double (64-bit) precision
Algorithm Details:
- Uses logarithmic calculation for normal number exponents
- Implements subnormal scaling via 2^(bias-1) multiplication
- Employs 5 extension bits for intermediate precision during calculation
- Integrates IEEE 754 compliant rounding with guard/round/sticky bits
- Validates output length and detects overflow/underflow conditions
Special Value Handling:
- Zero: Detected via direct comparison, returns appropriate zero pattern
- NaN: Detected using self-inequality property (NaN != NaN)
- Infinity: Detected via math.isinf(), returns infinity bit pattern
- Sign: Handled uniformly across all value types
Args:
num: Float or int number to convert
bit_len: Target bit length (32 for single, 64 for double precision)
Returns:
String representation of the binary number in IEEE 754 format
Format: [sign][exponent][mantissa] with exact bit_len length
Raises:
ValueError: If bit_len is not 32 or 64
BufferError: If overflow, underflow, or output length mismatch occurs
- Overflow: When exponent exceeds maximum representable value
- Underflow: When exponent is below minimum subnormal range
- Length mismatch: When output doesn't match expected bit_len
Implementation Notes:
- Bit arrays are processed in LSB-first order for arithmetic operations
- Mantissa extraction uses iterative fraction doubling algorithm
- Subnormal detection uses threshold 2^(1-bias) for range boundaries
- Extension bits provide extra precision before final rounding step
- All intermediate calculations maintain bit-level accuracy
"""
#Declare the sign bit variable
sign_bit: str = ""
#Handle the 32 or 64 bit numbers
if bit_len == 32:
exp_len: int = 8
mant_len: int = 23
exp_bias: int = 127
min_exp: int = -149
exp_format: str = '08b'
elif bit_len == 64:
exp_len: int = 11
mant_len: int = 52
exp_bias: int = 1023
min_exp: int = -1074
exp_format: str = '011b'
else:
raise ValueError(f"float_to_binary: Unsupported bit length, 32 or 64 expected, {bit_len} given.")
#Handle the main edge cases early
#Handle the sign bit
if num < 0:
sign_bit = "1"
else:
sign_bit = "0"
#Handle the 0.0 case
if num == 0.0:
return sign_bit + "0" * (bit_len -1)
#Handle NaN:
#Take advantage of a unique property of NaN: it's the only value that is not equal to itself. According to IEEE 754:
#For any normal number x, x == x is always true
#For NaN, NaN == NaN is always false
if num != num:
return sign_bit + ('1' * exp_len) + '1' + ('0' * (mant_len - 1))
#Handle infinity
#I choose to use the test from the math library
if math.isinf(num):
return sign_bit + ('1' * exp_len) + ('0' * mant_len)
#Declare main common variables
abs_num: float = abs(num)
output_bin_string: str = ""
extension_bit_number: int = 5
rounding_bits: str = ""
#Separate normal and subnormal range handling for building the exponent and mantissa
if abs_num < 2**(1-exp_bias): #subnormal numbers
subnorm_bin: str = ""
#exponent for subnormal numbers
biased_exponent: int = 0 #for subnormal numbers the exponent field is supposed to be all 0s
#fraction for subnormal numbers
# For subnormals the stored value is: abs_num = (0.b1b2…bₘ)₂ × 2^(1−bias)
# so to extract the pure fraction bits 0.b1b2… we divide out 2^(1−bias),
# i.e. multiply by 2^(bias−1). The binary “.xxxxx…” expansion of that
# product yields exactly the mantissa bits for the subnormal case.
scaled_fraction: float = abs_num * (2 ** (exp_bias - 1))
while scaled_fraction != 0 and len(subnorm_bin) < (mant_len + extension_bit_number):
scaled_fraction *= 2
bit: int = int(scaled_fraction)
subnorm_bin += str(bit)
scaled_fraction -= bit
#mantissa for subnormal numbers
mantissa: str = subnorm_bin.ljust(mant_len, '0')
else: #normal numbers
fractionP_bin: str = ""
#exponent for normal numbers
exponent: int = math.floor(math.log2(abs_num))
biased_exponent: int = exponent + exp_bias
if exponent > exp_bias:
raise BufferError(f"float_to_binary: overflow detected.")
elif exponent < min_exp:
raise BufferError(f"float_to_binary: underflow detected.")
#fraction for normal numbers - formula for the mantissa number = 1.mantissa × 2^(exponent) -> 1.mantissa = number / 2^(exponent)
norm_farction: float = abs_num / (2 ** exponent) #Normalize the whole number so the mantissa (1.xxx) can be extracted
fraction_mult: float = norm_farction - 1 #get the fractional part of the normalized mantissa
while fraction_mult != 0 and len(fractionP_bin) < (mant_len + extension_bit_number):
fraction_mult *= 2
bit: int = int(fraction_mult)
fractionP_bin += str(bit)
fraction_mult -= bit
#mantissa for normal numbers (already normalized so no need to drop a leading 1)
mantissa: str = fractionP_bin.ljust(mant_len, '0')
#Translate the biased exponent to binary
biased_exp_bin: str = format(biased_exponent, exp_format)
#Pad mantissa if needed or round as necessary
if len(mantissa) < mant_len:
padding: str = "".join("0" for _ in range(mant_len - len(mantissa)))
mantissa += padding
elif len(mantissa) > mant_len:
rounding_bits = mantissa[mant_len:]
mantissa = mantissa[0 : mant_len]
biased_exp_bin, mantissa = self.float_rounder(exponent = biased_exp_bin, mantissa = mantissa, rounding_bits = rounding_bits)
#Build the final output binary and check the output length
output_bin_string = sign_bit + biased_exp_bin + mantissa
if len(output_bin_string) != bit_len:
raise BufferError(f"float_to_binary: the output bit len was expected to be {bit_len}, {len(output_bin_string)} was received")
return output_bin_string
def int_to_bits(self, input_int: int, bit_len: int = 64) -> list[int]:
"""
Convert an integer to its binary representation as a list of bits.
Creates a binary representation with the least significant bit (LSB) first,
which facilitates easier bit-level arithmetic operations. The function applies
a width mask to ensure the output is exactly the specified bit length.
Args:
input_int: The integer to convert to binary representation
bit_len: The bit width to use (default: 64)
Returns:
A list of bits in LSB->MSB order (least significant bit first)
Notes:
- Applies a width mask to truncate values exceeding bit_len
- LSB-first ordering simplifies carry propagation in arithmetic operations
- Used extensively in floating-point exponent and mantissa processing
Example:
int_to_bits(5, 4) returns [1, 0, 1, 0] representing binary 0101
"""
width_mask: int = (1 << bit_len) -1 #creates a mask of 64 1s to cut the target to 64 bits
bit_seq: list[int] = []
masked_input = input_int & width_mask
for bit_index in range(bit_len):
bit_seq.append((masked_input >> bit_index) & 1)
return bit_seq
def bit_to_int(self, input_bits: list[int], signed: bool) -> int:
"""
Convert a list of bits to a signed or unsigned integer.
Reconstructs an integer value from a bit list in LSB->MSB order.
Handles both signed (two's complement) and unsigned interpretations.
Critical for converting computed bit sequences back to numeric values
during floating-point operations.
Args:
input_bits: The bit list to convert (LSB->MSB order)
signed: If True, interprets MSB as sign bit using two's complement
Returns:
The integer value represented by the bit list
Notes:
- For signed=True: Uses two's complement representation
- For signed=False: Standard unsigned binary interpretation
- Essential for exponent bias calculations and overflow detection
- Handles negative results correctly for subnormal exponent calculations
Example:
bit_to_int([1, 0, 1, 0], False) returns 5 (unsigned)
bit_to_int([1, 1, 1, 1], True) returns -1 (signed two's complement)
"""
bit_string: int = 0
for i, bit in enumerate(input_bits):
mask: int = bit << i #create a mask by pushing the given bit to the given position
bit_string = bit_string | mask #OR the mash with the bit string
#Check the signed mode
if signed == True and input_bits[-1] == 1: #if in signed mode and the sign bit is 1 convert to 2's complement
return bit_string - (1 << len(input_bits)) #bit string - the max position value (1024 for 10 bits, 8192 for 13 bits)
else:
return bit_string
def fp_twos_complement(self, bit_seq: list[int]) -> list[int]:
"""
Convert a bit sequence to its two's complement representation.
Implements two's complement arithmetic by inverting all bits and adding 1.
Essential for subtraction operations in floating-point arithmetic,
particularly for bias subtraction in exponent calculations.
Uses full_adder for proper carry propagation.
Args:
bit_seq: A bit sequence in LSB->MSB order to convert
Returns:
Two's complement representation as a bit list (LSB->MSB order)
Notes:
- Step 1: Invert all bits (one's complement)
- Step 2: Add 1 using full_adder with carry propagation
- Used primarily in sub_bias() for exponent bias subtraction
- Maintains LSB->MSB ordering for arithmetic consistency
Algorithm:
1. Create one's complement by inverting each bit
2. Add 1 to the inverted sequence using full_adder
3. Propagate carry through all bit positions
"""
complement_seq: list[int] = []
twoC_seq: list[int] = []
carry_over: int = 0
for bit in bit_seq:
if bit == 0:
complement_seq.append(1)
else:
complement_seq.append(0)
for i, bit in enumerate(complement_seq):
new_bit: int = 0
if i == 0:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = 1)
twoC_seq.append(new_bit)
else:
new_bit, carry_over = self.full_adder(input_a = bit, carry_in = carry_over)
twoC_seq.append(new_bit)
return twoC_seq
def fp_bytearray_to_bitlist(self, number: bytearray | bytes, var_type: str, bit_length: int = 64, bias: int = 1023) -> list[int]:
"""
Takes in a bytearray (little endian) representing a floating point number or integer fetched from a register and turns it into a big endian bitlist for processing in the FPU
"""
if not isinstance(number, (bytearray, bytes)) and (var_type == "float" or var_type == "int"):
raise FpuError(f"fp_bytearray_to_bitlist: expected a variable of type float, got fp_number: {type(number)} with var_type: {var_type}")
#Flip the bytearray to big endian for the FPU operations
num: bytearray | bytes = number[::-1]
#Declare variables and convert the bytearray to a bitstring
bitstring: str = ""
for bit in num:
bitstring += format(bit, "08b")
#Convert the bytearray to a bitlist
bitlist: list[int] = [int(bit) for bit in bitstring]
if var_type == "float":
return bitlist
#Define the components for int to float conversion
sing_bit: str = ""
exponent_seq: str = ""
mantissa_seq: str = ""
rounding_bit_seq: str = ""
#Extract the sign bit for the int conversion
sing_bit = str(bitlist[0])
#As my ints are signed, the negative ints are in two's complement form so I have to switch them back to unsigned for the conversion
#technically taking the absolute value
if sing_bit == "1":
#NOTE: the function takes a list in LSB -> MSB order, therefore I flip the list in the input
abs_bitlist: list[int] = self.fp_twos_complement(bitlist[::-1]) #convert negative numbers into unsigned versions (equivalent to abs)
abs_bitlist = abs_bitlist[::-1] #flip back the list to MSB -> LSB order for further operations
else:
abs_bitlist = bitlist.copy()
#For integers pad tem to 64 bits if they are not 64 bit ints
if len(abs_bitlist) < bit_length:
for bit in range(bit_length - len(abs_bitlist)):
abs_bitlist.insert(0, 0)
#Find the MSB position
msb_offset: int = 0 #I have to search for the set MSB form the MSB side, but I need the bit position form the LSB side hence the offset
for bit_index, bit in enumerate(abs_bitlist):
if bit ^ 1 == 0:
msb_offset = bit_index
break
#Calculate where the decimal point should move for the normalization (this will be new unbiased exponent)
msb_index: int = (bit_length - msb_offset) - 1 #MSB position form the LSB end of the string with 0 indexing = exponent
#Convert the ex to Ex by adding the bias and turn it into a bitstring
biased_exponent: int = msb_index + bias
if bit_length == 32:
exponent_seq = format(biased_exponent, "08b")
else:
exponent_seq = format(biased_exponent, "011b")
#Extract the bits after the decimal point to get the mantissa
fractional_bits: list[int] = abs_bitlist[msb_offset + 1:] #the msb_index 1 is the hidden bit
#Set the mantissa length
if bit_length == 32:
mantissa_len: int = 23
else:
mantissa_len: int = 52
#Define the rounding bits based on the mantissa length
if len(fractional_bits) < mantissa_len:
rounding_bits: list[int] = [0] * 5 #if the length is less than the given mantissa length the rounding bits are all 0s
for bit in rounding_bits:
rounding_bit_seq += str(bit) #convert the rounding bit list into a bit string for the float_rounder function
for _ in range(mantissa_len - len(fractional_bits)):
fractional_bits.append(0)
for bit in fractional_bits:
mantissa_seq += str(bit)
elif len(fractional_bits) > mantissa_len:
rounding_bits: list[int] = fractional_bits[mantissa_len + 1:]
for bit in rounding_bits:
rounding_bit_seq += str(bit) #convert the rounding bit list into a bit string for the floar_rounder function
for bit in fractional_bits[:mantissa_len]:
mantissa_seq += str(bit)
#Round the new mantissa and exponent based on the rounding bits
rounded_exp, rounded_mant = self.float_rounder(exponent = exponent_seq, mantissa = mantissa_seq, rounding_bits = rounding_bit_seq)
#Build the new floating point number
int_bitstring: str = sing_bit + rounded_exp + rounded_mant
#Convert the bitstring into a bitlist
int_bitlist: list[int] = [int(bit) for bit in int_bitstring]
return int_bitlist
def add_sub_bits(self, bit_list_1: list[int], bit_list_2: list[int]) -> list[int]:
#Equalize the length of the bit lists by padding
if len(bit_list_1) != len(bit_list_2):
if len(bit_list_1) < len(bit_list_2):
bit_list_1.extend([0 for _ in range(len(bit_list_2) - len(bit_list_1))])
else:
bit_list_2.extend([0 for _ in range(len(bit_list_1) - len(bit_list_2))])
#Declare variables
bit_length: int = len(bit_list_1)
new_seq: list[int] = []
carry_over: int = 0
msb_in: int = 0
#Bitwise addition/subtraction if bit_list_2 is in 2's C form
for bit_index in range(bit_length):
new_bit: int = 0
if bit_index == (bit_length - 1):
msb_in: int = carry_over
new_bit, carry_over = self.full_adder(input_a = bit_list_1[bit_index], input_b = bit_list_2[bit_index], carry_in = carry_over)
new_seq.append(new_bit)
#Check for overflow
if msb_in != carry_over:
raise FpuError(message="add_sub_bits: overflow detected at the end of exponent subtraction")
return new_seq
def add_sub_exponents(self, exponent_1: list[int], exponent_2: list[int], bias: int, intermediate_len: int, final_len: int, mode: str,
subnormal_operand_1: bool, subnormal_operand_2: bool, nlz_operand_1: int, nlz_operand_2: int) -> tuple[list[int], int]:
"""
A function to add or subtract the unbiased exponents during floating point operations multiply and divide respectively.
It returns a tuple with the new exponent (MSB -> LSB) and the number of leading zeros.
"""
#Transfer ownership of the exponents
exp_1: list[int] = exponent_1.copy()
exp_2: list[int] = exponent_2.copy()
#MSB -> LSB to LSB -> MSB conversion for easier calculations
exp_1.reverse()
exp_2.reverse()
#Pad the exponents to the intermediate bit length
if len(exp_1) != intermediate_len or len(exp_2) != intermediate_len:
exp_1.extend([0 for _ in range(intermediate_len - len(exp_1))])
exp_2.extend([0 for _ in range(intermediate_len - len(exp_2))])
#Convert the bias and nlz_counts to a bit lists for operations (LSB -> MSB)
nlz_dividend_seq: list[int] = self.int_to_bits(input_int = nlz_operand_1, bit_len = intermediate_len)
nlz_divisor_seq: list[int] = self.int_to_bits(input_int = nlz_operand_2, bit_len = intermediate_len)
bias_seq: list[int] = self.int_to_bits(input_int = bias, bit_len = intermediate_len)
#Unbias the stored exponents for the exponent operation by removing the bias
if subnormal_operand_1 == True and subnormal_operand_2 == False: #unbiased subnormal exponent is 1 - (bias + nlz_count)
#Deal with the subnormal operand
exp_1 = self.int_to_bits(input_int = 1, bit_len = intermediate_len) #set the operand 1 for the proper bias removal
adjusted_bias_dividend: list[int] = self.add_sub_bits(bit_list_1 = bias_seq, bit_list_2 = nlz_dividend_seq) #calculate the nlz_count adjusted bias
bias_seq_dividend_2c: list[int] = self.fp_twos_complement(bit_seq = adjusted_bias_dividend) #transform the bias to two's complement for subtraction
unbiased_exp_1: list[int] = self.add_sub_bits(bit_list_1 = exp_1, bit_list_2 = bias_seq_dividend_2c)
#Deal with the normal operand
bias_seq_2c: list[int] = self.fp_twos_complement(bit_seq = bias_seq) #transform the bias to two's complement for subtraction
unbiased_exp_2: list[int] = self.add_sub_bits(bit_list_1 = exp_2, bit_list_2 = bias_seq_2c)
elif subnormal_operand_1 == False and subnormal_operand_2 == True: #unbiased subnormal exponent is 1 - (bias + nlz_count)
#Deal with the subnormal operand
exp_2 = self.int_to_bits(input_int = 1, bit_len = intermediate_len) #set the operand 2 for the proper bias removal
adjusted_bias_divisor: list[int] = self.add_sub_bits(bit_list_1 = bias_seq, bit_list_2 = nlz_divisor_seq) #calculate the nlz_count adjusted bias
bias_seq_divisor_2c: list[int] = self.fp_twos_complement(bit_seq = adjusted_bias_divisor) #transform the bias to two's complement for subtraction
unbiased_exp_2: list[int] = self.add_sub_bits(bit_list_1 = exp_2, bit_list_2 = bias_seq_divisor_2c)
#Deal with the normal operand
bias_seq_2c: list[int] = self.fp_twos_complement(bit_seq = bias_seq) #transform the bias to two's complement for subtraction
unbiased_exp_1: list[int] = self.add_sub_bits(bit_list_1 = exp_1, bit_list_2 = bias_seq_2c)
elif subnormal_operand_1 == True and subnormal_operand_2 == True:
#Deal with the subnormal operands
exp_1 = self.int_to_bits(input_int = 1, bit_len = intermediate_len) #set the operand 1 for the proper bias removal
exp_2 = self.int_to_bits(input_int = 1, bit_len = intermediate_len) #set the operand 2 for the proper bias removal
adjusted_bias_dividend: list[int] = self.add_sub_bits(bit_list_1 = bias_seq, bit_list_2 = nlz_dividend_seq) #calculate the nlz_count adjusted bias
adjusted_bias_divisor: list[int] = self.add_sub_bits(bit_list_1 = bias_seq, bit_list_2 = nlz_divisor_seq) #calculate the nlz_count adjusted bias
bias_seq_dividend_2c: list[int] = self.fp_twos_complement(bit_seq = adjusted_bias_dividend) #transform the bias to two's complement for subtraction
unbiased_exp_1: list[int] = self.add_sub_bits(bit_list_1 = exp_1, bit_list_2 = bias_seq_dividend_2c)
bias_seq_divisor_2c: list[int] = self.fp_twos_complement(bit_seq = adjusted_bias_divisor) #transform the bias to two's complement for subtraction
unbiased_exp_2: list[int] = self.add_sub_bits(bit_list_1 = exp_2, bit_list_2 = bias_seq_divisor_2c)
else:
#Transform the bias to two's complement for subtraction
bias_seq_2c = self.fp_twos_complement(bit_seq = bias_seq)
#Subtract the bias from the stored exponents (Ex - bias = ex)
unbiased_exp_1: list[int] = self.add_sub_bits(bit_list_1 = exp_1, bit_list_2 = bias_seq_2c)
unbiased_exp_2: list[int] = self.add_sub_bits(bit_list_1 = exp_2, bit_list_2 = bias_seq_2c)
#Transform unbiased exponent 2 to two's complement form for the subtraction
if mode == "sub":
unbiased_exp_2 = self.fp_twos_complement(bit_seq = unbiased_exp_2)
elif mode == "add":
pass
else:
raise FpuError(f"add_sub_exponents: the given mode has to be either add or sub, {mode} was given.")
#Calculate the new stored exponent using the formula (ex - ey) + bias or (ex + ey) + bias, depending on the mode
new_seq: list[int] = self.add_sub_bits(bit_list_1 = unbiased_exp_1, bit_list_2 = unbiased_exp_2)
biased_new_seq = self.add_sub_bits(bit_list_1 = new_seq, bit_list_2 = bias_seq)
#Calculate the new exponent to check if the result is a subnormal number and define the mantissa shift subnormals
biased_new_exponent: int = self.bit_to_int(input_bits = biased_new_seq, signed = True) #this is the |Ex-Ey+bias| or |Ex+Ey+bias| part of the shift calculation
mantissa_shift: int = 0
#detect a subnormal result by checking if the extended new biased exponent is less than the lowest normal exponent
if biased_new_exponent - bias < (1-bias):
output: list[int] = [0 for _ in range(final_len)] #Subnormal exponent pattern: all 0s
output: list[int] = output[::-1]
#If the result is subnormal calculate the necessary mantissa shift using the formula |Ex-Ey+bias| + 1 or |Ex+Ey+bias| + 1
mantissa_shift: int = abs(biased_new_exponent) + 1
return output, mantissa_shift
#detect overflow which should produce an Inf value
if biased_new_seq[final_len : len(biased_new_seq)].count(1) != 0: #there are 1s over the exponent bit limit
output: list[int] = [1 for _ in range(final_len)] #Inf exponent pattern: all 1s
output: list[int] = output[::-1]
return output, mantissa_shift
else:
output: list[int] = biased_new_seq[0:final_len]
output: list[int] = output[::-1]
return output, mantissa_shift
#NOTE: Floating point multiplication unique internal methods start here
def add_biased_exponents(self, exponent_1: list[int], exponent_2: list[int], intermediate_len: int) -> list[int]:
"""
Add two biased IEEE 754 exponents for floating-point multiplication.
Performs binary addition of two exponent bit sequences with overflow detection.
The exponents are added in their biased form (no bias removal required).
This is the first step in the IEEE 754 multiplication formula:
new_exp = (exp1 + exp2) - bias.
Args:
exponent_1: First exponent as list of bits in MSB->LSB order
exponent_2: Second exponent as list of bits in MSB->LSB order
intermediate_len: Target bit length for intermediate calculations (includes overflow protection)
Returns:
Sum of exponents as list of bits in LSB->MSB order, padded to intermediate_len
Raises:
FpuError: If arithmetic overflow is detected during addition
Notes:
- Input exponents are automatically zero-padded to intermediate_len
- Reverses inputs to LSB->MSB for easier bit-by-bit addition
- Uses full_adder for proper carry propagation
- Overflow detection compares MSB input carry with final carry
- Output remains in LSB->MSB order for subsequent bias subtraction
Algorithm Flow:
1. Copy and reverse input exponents (MSB->LSB to LSB->MSB)
2. Zero-pad both exponents to intermediate_len
3. Perform bit-by-bit addition with carry propagation
4. Check for overflow by comparing MSB carry states
"""
#Borrow the exponents
exp_1: list[int] = exponent_1.copy()
exp_2: list[int] = exponent_2.copy()
#Reverse the exponents for easier bitwise operations
exp_1.reverse()
exp_2.reverse()
#Declare variables
carry_over: int = 0
new_seq: list[int] = []
msb_in: int = 0
#Pad the exponents to the intermediate bit length
if len(exp_1) != intermediate_len or len(exp_2) != intermediate_len:
exp_1.extend([0 for _ in range(intermediate_len - len(exp_1))])
exp_2.extend([0 for _ in range(intermediate_len - len(exp_2))])
#Do bitwise addition with the full adder
for bit_index in range(intermediate_len):
new_bit: int = 0
if bit_index == (intermediate_len - 1):
msb_in = carry_over
new_bit, carry_over = self.full_adder(input_a = exp_1[bit_index], input_b = exp_2[bit_index], carry_in = carry_over)
new_seq.append(new_bit)
#Check for overflow
if msb_in != carry_over:
raise FpuError(message="add_biased_exponents: overflow detected at the end of exponent addition")
return new_seq
def sub_bias(self, exponent_seq: list[int], bias: int, intermediate_len: int, final_len: int, subnormal: bool) -> tuple[list[int], int]:
"""
Subtract the IEEE 754 exponent bias from a biased exponent sum with subnormal handling.
Completes the IEEE 754 exponent calculation: final_exp = (exp1 + exp2) - bias.
Includes comprehensive subnormal number detection and handling:
- Detects when results fall below the normal range
- Calculates mantissa right-shift amounts for subnormal results
- Handles overflow detection for infinity generation
- Supports both normal and subnormal input number bias adjustments
Args:
exponent_seq: Biased exponent sum as list of bits in LSB->MSB order
bias: IEEE 754 bias value (127 for 32-bit, 1023 for 64-bit)
intermediate_len: Bit length of input exponent sequence
final_len: Target bit length for final exponent output
subnormal: If True, uses modified bias for subnormal input handling
Returns:
Tuple containing:
- Unbiased exponent as list of bits in MSB->LSB order, trimmed to final_len
- Mantissa shift amount (0 for normal, >0 for subnormal results)
Raises:
FpuError: If arithmetic overflow is detected during subtraction
Notes:
- For subnormal inputs: bias = 1 - (original_bias + leading_zeros)
- For normal inputs: bias = original_bias
- Subnormal detection: checks if result < (1 - bias)
- Subnormal output: exponent = all zeros, shift = |result| + 1
- Overflow detection: checks for bits beyond final_len
Subnormal Handling Details:
- Detects subnormal results by comparing unbiased exponent with minimum normal
- Calculates mantissa right-shift using formula: |exp + bias| + 1
- Returns all-zero exponent pattern for subnormal results
- Maintains IEEE 754 compliance for gradual underflow
Algorithm Flow:
1. Create appropriate bias (normal or subnormal-adjusted)
2. Convert bias to two's complement for subtraction
3. Perform bit-by-bit subtraction with overflow detection
4. Check for subnormal result and calculate shift if needed
5. Check for overflow and return infinity pattern if needed
6. Return final exponent and mantissa shift amount
"""
if subnormal == True: #for subnormal numbers we use the Ex + Ey (done before) - (1-[127 + nlz]) formula to get a normalized subnormal exponent
bias_seq: list[int] = self.int_to_bits(input_int = (1 - bias), bit_len = intermediate_len)
bias_2c: list[int] = bias_seq #as the generated bias is negative, it is already in two's complement form after translation into a bit seq
else:
bias_seq: list[int] = self.int_to_bits(input_int = bias, bit_len = intermediate_len)
bias_2c: list[int] = self.fp_twos_complement(bit_seq = bias_seq)
carry_over: int = 0
new_seq: list[int] = []
msb_in: int = 0
for bit_index in range(intermediate_len):
new_bit: int = 0
if bit_index == (intermediate_len - 1):
msb_in = carry_over
new_bit, carry_over = self.full_adder(input_a = exponent_seq[bit_index], input_b = bias_2c[bit_index], carry_in = carry_over)
new_seq.append(new_bit)
if msb_in != carry_over:
raise FpuError(message="sub_bias: overflow detected at the end of bias subtraction")
#Calculate the new unbiased exponent to check if the result is a subnormal number and define the mantissa shift subnormals
biased_new_exponent: int = self.bit_to_int(input_bits = new_seq, signed = False) #this is the |ex+ey-bias| part of the shift calculation
mantissa_shift: int = 0
#detect a subnormal result by checking if the extended new biased exponent is less than the lowest normal exponent
if biased_new_exponent - bias < (1-bias):
output: list[int] = [0 for _ in range(final_len)] #Subnormal exponent pattern: all 0s
output = output[::-1]
#If the result is subnormal calculate the necessary mantissa shift using the formula |ex+ey-bias| + 1
mantissa_shift: int = abs(biased_new_exponent) + 1
#mantissa_shift = mantissa_shift
return output, mantissa_shift
#detect overflow which should produce an Inf value