CharlesCNorton commited on
Commit
8260d68
·
1 Parent(s): 22baf26

float add: full IEEE subnormal support (subnormal operands and gradual-underflow results) via effective exponent, implicit-bit selection, and a clamped normalize shift; oracle validated bit-exact against numpy float32/float16

Browse files
Files changed (2) hide show
  1. src/build.py +67 -16
  2. src/eval.py +48 -34
src/build.py CHANGED
@@ -1097,9 +1097,9 @@ def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1097
  Self-contained circuit; external inputs are the raw operand words
1098
  $a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
1099
  (NaN, infinities, signed zeros, opposite-sign infinities -> NaN, exact
1100
- cancellation -> +0), flush-to-zero for subnormal operands and results,
1101
- round-to-nearest-even for the mantissa. A zero operand passes the other
1102
- operand through verbatim.
1103
 
1104
  Datapath: payload comparison picks the larger-magnitude operand, swap
1105
  muxes route it to L; the smaller mantissa is aligned right through a
@@ -1132,6 +1132,13 @@ def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1132
  add_gate(tensors, f"{prefix}.{op}.frac_zero", [-1.0] * F, [0.0])
1133
  add_gate(tensors, f"{prefix}.{op}.is_nan", [1.0, 1.0], [-2.0])
1134
  add_gate(tensors, f"{prefix}.{op}.is_inf", [1.0, 1.0], [-2.0])
 
 
 
 
 
 
 
1135
 
1136
  # Payload magnitude comparison (chooses the larger-magnitude operand).
1137
  add_bit_cascade_compare(
@@ -1148,7 +1155,7 @@ def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1148
  # Swap muxes: sel = pl.lt (a smaller in magnitude -> L = b).
1149
  add_gate(tensors, f"{prefix}.swap.not_sel", [-1.0], [0.0])
1150
  for name, width in (("sign_l", 1), ("exp_l", E), ("exp_s", E),
1151
- ("mant_l", F), ("mant_s", F)):
1152
  for k in range(width):
1153
  g = f"{prefix}.swap.{name}.bit{k}" if width > 1 else f"{prefix}.swap.{name}"
1154
  add_gate(tensors, f"{g}.and_a", [1.0, 1.0], [-2.0])
@@ -1248,6 +1255,20 @@ def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1248
  add_gate(tensors, f"{prefix}.exp_round.bit{k}.xor.layer2", [1.0, 1.0], [-2.0])
1249
  add_gate(tensors, f"{prefix}.exp_round.bit{k}.carry", [1.0, 1.0], [-2.0])
1250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1251
  add_gate(tensors, f"{prefix}.exp_r.zero", [-1.0] * (E + 2), [0.0])
1252
  add_gate(tensors, f"{prefix}.exp_r.underflow", [1.0, 1.0], [-1.0])
1253
  add_gate(tensors, f"{prefix}.exp_r.not_neg", [-1.0], [0.0])
@@ -1269,7 +1290,7 @@ def add_float_add(tensors: Dict[str, torch.Tensor], family: str,
1269
  add_gate(tensors, f"{prefix}.sel.a_zero_pass", [1.0, 1.0, 1.0, 1.0], [-4.0])
1270
  add_gate(tensors, f"{prefix}.sel.b_zero_pass", [1.0, 1.0, 1.0, 1.0], [-4.0])
1271
  add_gate(tensors, f"{prefix}.sel.dp_reach", [1.0, 1.0, 1.0, 1.0], [-4.0])
1272
- add_gate(tensors, f"{prefix}.sel.dp_zero_c", [1.0, 1.0], [-1.0])
1273
  add_gate(tensors, f"{prefix}.sel.not_dp_zero_c", [-1.0], [0.0])
1274
  add_gate(tensors, f"{prefix}.sel.not_ovf", [-1.0], [0.0])
1275
  add_gate(tensors, f"{prefix}.sel.dp_zero", [1.0, 1.0], [-2.0])
@@ -3444,14 +3465,14 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3444
  # bit F+2 implicit 1.
3445
  def l_ext(k: int) -> int:
3446
  if k == F + 2:
3447
- return one
3448
  if 2 <= k <= F + 1:
3449
  return mant_l(k - 2)
3450
  return zero
3451
 
3452
  def s_frame_in(k: int) -> int:
3453
  if k == F + 2:
3454
- return one
3455
  if 2 <= k <= F + 1:
3456
  return mant_s(k - 2)
3457
  return zero
@@ -3489,11 +3510,17 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3489
  "a.frac_nz": frac_a_field, "a.frac_zero": frac_a_field,
3490
  "a.is_nan": [R("a.exp_max"), R("a.frac_nz")],
3491
  "a.is_inf": [R("a.exp_max"), R("a.frac_zero")],
 
 
 
3492
  "b.exp_zero": exp_b_field, "b.exp_nzero": exp_b_field,
3493
  "b.exp_max": exp_b_field,
3494
  "b.frac_nz": frac_b_field, "b.frac_zero": frac_b_field,
3495
  "b.is_nan": [R("b.exp_max"), R("b.frac_nz")],
3496
  "b.is_inf": [R("b.exp_max"), R("b.frac_zero")],
 
 
 
3497
  "swap.not_sel": [R("pl.lt")],
3498
  "align.not_sat": [R("align.sat")],
3499
  "align.sticky_final": [R(f"align.s{num_stages - 1}.sticky"), R("align.sat")],
@@ -3506,6 +3533,9 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3506
  "round.tail": [shifted(1), shifted(0), R("align.sticky_final")],
3507
  "round.rsl": [R("round.tail"), trunc(0)],
3508
  "round.up": [shifted(2), R("round.rsl")],
 
 
 
3509
  "exp_r.zero": [R(f"exp_round.bit{k}.xor.layer2") for k in range(E + 2)],
3510
  "exp_r.underflow": [R(f"exp_round.bit{E + 1}.xor.layer2"), R("exp_r.zero")],
3511
  "exp_r.not_neg": [R(f"exp_round.bit{E + 1}.xor.layer2")],
@@ -3520,14 +3550,14 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3520
  "sel.not_inf_ops": [R("sel.inf_ops")],
3521
  "sel.inf_operand": [R("sel.not_nan"), R("sel.inf_ops")],
3522
  "sel.both_zero": [R("sel.not_nan"), R("sel.not_inf_ops"),
3523
- R("a.exp_zero"), R("b.exp_zero")],
3524
  "sel.a_zero_pass": [R("sel.not_nan"), R("sel.not_inf_ops"),
3525
- R("a.exp_zero"), R("b.exp_nzero")],
3526
  "sel.b_zero_pass": [R("sel.not_nan"), R("sel.not_inf_ops"),
3527
- R("b.exp_zero"), R("a.exp_nzero")],
3528
  "sel.dp_reach": [R("sel.not_nan"), R("sel.not_inf_ops"),
3529
- R("a.exp_nzero"), R("b.exp_nzero")],
3530
- "sel.dp_zero_c": [R("lzc.not_nz"), R("exp_r.underflow")],
3531
  "sel.not_dp_zero_c": [R("sel.dp_zero_c")],
3532
  "sel.not_ovf": [R("exp_r.overflow")],
3533
  "sel.dp_zero": [R("sel.dp_reach"), R("sel.dp_zero_c")],
@@ -3585,7 +3615,7 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3585
  return [R("pl.le.not_gt")]
3586
 
3587
  # Swap muxes: sel = pl.lt; L side takes b when sel, S side takes a.
3588
- m = re.match(r"^swap\.(sign_l|exp_l|exp_s|mant_l|mant_s)(?:\.bit(\d+))?\.(and_a|and_b|or)$", suffix)
3589
  if m:
3590
  name = m.group(1)
3591
  k = int(m.group(2)) if m.group(2) is not None else 0
@@ -3594,7 +3624,11 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3594
  if name == "sign_l":
3595
  a_side, b_side = a(0), b(0)
3596
  elif name in ("exp_l", "exp_s"):
3597
- a_side, b_side = a(E - k), b(E - k)
 
 
 
 
3598
  else:
3599
  a_side, b_side = a(W - 1 - k), b(W - 1 - k)
3600
  l_side = name.endswith("_l") or name == "sign_l"
@@ -3702,7 +3736,7 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3702
  bbit = int(m.group(1))
3703
  k = int(m.group(2))
3704
  kind = m.group(3)
3705
- sel = R(f"lzc.nlz.bit{bbit}")
3706
 
3707
  def nin(kk: int) -> int:
3708
  if kk < 0 or kk >= RES:
@@ -3737,7 +3771,7 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3737
  k = int(m.group(1))
3738
  rest = m.group(2)
3739
  cin = R(f"rnd.bit{F - 1}.carry") if k == 0 else R(f"exp_round.bit{k - 1}.carry")
3740
- e_bit = R(f"exp_r.fa{k}.ha2.sum.layer2")
3741
  if rest == "xor.layer1.or" or rest == "xor.layer1.nand":
3742
  return [e_bit, cin]
3743
  if rest == "xor.layer2":
@@ -3749,6 +3783,23 @@ def infer_float_add_inputs(gate: str, reg: SignalRegistry, family: str,
3749
  if m:
3750
  return [R(f"lzc.nlz.bit{int(m.group(1))}")]
3751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3752
  m = re.match(r"^exp_s1\.fa(\d+)\.(.+)$", suffix)
3753
  if m:
3754
  k = int(m.group(1))
 
1097
  Self-contained circuit; external inputs are the raw operand words
1098
  $a[0..W-1] / $b[0..W-1] (MSB-first). Contract: exact IEEE specials
1099
  (NaN, infinities, signed zeros, opposite-sign infinities -> NaN, exact
1100
+ cancellation -> +0), subnormal operands and gradual-underflow subnormal
1101
+ results, round-to-nearest-even for the mantissa. A zero operand passes the
1102
+ other operand through verbatim.
1103
 
1104
  Datapath: payload comparison picks the larger-magnitude operand, swap
1105
  muxes route it to L; the smaller mantissa is aligned right through a
 
1132
  add_gate(tensors, f"{prefix}.{op}.frac_zero", [-1.0] * F, [0.0])
1133
  add_gate(tensors, f"{prefix}.{op}.is_nan", [1.0, 1.0], [-2.0])
1134
  add_gate(tensors, f"{prefix}.{op}.is_inf", [1.0, 1.0], [-2.0])
1135
+ # True zero is exp==0 AND frac==0; a subnormal (exp==0, frac!=0) is a
1136
+ # nonzero datapath operand, not a zero.
1137
+ add_gate(tensors, f"{prefix}.{op}.is_zero", [1.0, 1.0], [-2.0])
1138
+ add_gate(tensors, f"{prefix}.{op}.nonzero", [-1.0], [0.0])
1139
+ # Effective exponent LSB for subnormals: a subnormal (exp==0) acts at
1140
+ # the smallest normal exponent (biased 1), so force the LSB when exp==0.
1141
+ add_gate(tensors, f"{prefix}.{op}.eexp_lsb", [1.0, 1.0], [-1.0])
1142
 
1143
  # Payload magnitude comparison (chooses the larger-magnitude operand).
1144
  add_bit_cascade_compare(
 
1155
  # Swap muxes: sel = pl.lt (a smaller in magnitude -> L = b).
1156
  add_gate(tensors, f"{prefix}.swap.not_sel", [-1.0], [0.0])
1157
  for name, width in (("sign_l", 1), ("exp_l", E), ("exp_s", E),
1158
+ ("mant_l", F), ("mant_s", F), ("impl_l", 1), ("impl_s", 1)):
1159
  for k in range(width):
1160
  g = f"{prefix}.swap.{name}.bit{k}" if width > 1 else f"{prefix}.swap.{name}"
1161
  add_gate(tensors, f"{g}.and_a", [1.0, 1.0], [-2.0])
 
1255
  add_gate(tensors, f"{prefix}.exp_round.bit{k}.xor.layer2", [1.0, 1.0], [-2.0])
1256
  add_gate(tensors, f"{prefix}.exp_round.bit{k}.carry", [1.0, 1.0], [-2.0])
1257
 
1258
+ # Gradual underflow. exp_r <= 0 (pre-round) means the result is subnormal:
1259
+ # clamp the normalize shift to exp_L so it stays denormalized, and clamp the
1260
+ # exponent field to 0. The datapath's normal path then emits the subnormal.
1261
+ add_gate(tensors, f"{prefix}.er.zero", [-1.0] * (E + 2), [0.0])
1262
+ add_gate(tensors, f"{prefix}.er.underflow", [1.0, 1.0], [-1.0]) # OR(sign, zero)
1263
+ add_gate(tensors, f"{prefix}.er.not_underflow", [-1.0], [0.0])
1264
+ for k in range(E + 2):
1265
+ add_gate(tensors, f"{prefix}.exprc.bit{k}", [1.0, 1.0], [-2.0]) # AND(exp_r, not_uf)
1266
+ for bbit in range(nlz_bits): # nsh = min(nlz, exp_L)
1267
+ add_gate(tensors, f"{prefix}.nsh.bit{bbit}.not_sel", [-1.0], [0.0])
1268
+ add_gate(tensors, f"{prefix}.nsh.bit{bbit}.and_a", [1.0, 1.0], [-2.0])
1269
+ add_gate(tensors, f"{prefix}.nsh.bit{bbit}.and_b", [1.0, 1.0], [-2.0])
1270
+ add_gate(tensors, f"{prefix}.nsh.bit{bbit}.or", [1.0, 1.0], [-1.0])
1271
+
1272
  add_gate(tensors, f"{prefix}.exp_r.zero", [-1.0] * (E + 2), [0.0])
1273
  add_gate(tensors, f"{prefix}.exp_r.underflow", [1.0, 1.0], [-1.0])
1274
  add_gate(tensors, f"{prefix}.exp_r.not_neg", [-1.0], [0.0])
 
1290
  add_gate(tensors, f"{prefix}.sel.a_zero_pass", [1.0, 1.0, 1.0, 1.0], [-4.0])
1291
  add_gate(tensors, f"{prefix}.sel.b_zero_pass", [1.0, 1.0, 1.0, 1.0], [-4.0])
1292
  add_gate(tensors, f"{prefix}.sel.dp_reach", [1.0, 1.0, 1.0, 1.0], [-4.0])
1293
+ add_gate(tensors, f"{prefix}.sel.dp_zero_c", [1.0], [-1.0]) # true-zero result only
1294
  add_gate(tensors, f"{prefix}.sel.not_dp_zero_c", [-1.0], [0.0])
1295
  add_gate(tensors, f"{prefix}.sel.not_ovf", [-1.0], [0.0])
1296
  add_gate(tensors, f"{prefix}.sel.dp_zero", [1.0, 1.0], [-2.0])
 
3465
  # bit F+2 implicit 1.
3466
  def l_ext(k: int) -> int:
3467
  if k == F + 2:
3468
+ return R("swap.impl_l.or") # implicit bit: 1 normal, 0 subnormal
3469
  if 2 <= k <= F + 1:
3470
  return mant_l(k - 2)
3471
  return zero
3472
 
3473
  def s_frame_in(k: int) -> int:
3474
  if k == F + 2:
3475
+ return R("swap.impl_s.or")
3476
  if 2 <= k <= F + 1:
3477
  return mant_s(k - 2)
3478
  return zero
 
3510
  "a.frac_nz": frac_a_field, "a.frac_zero": frac_a_field,
3511
  "a.is_nan": [R("a.exp_max"), R("a.frac_nz")],
3512
  "a.is_inf": [R("a.exp_max"), R("a.frac_zero")],
3513
+ "a.eexp_lsb": [a(E), R("a.exp_zero")],
3514
+ "a.is_zero": [R("a.exp_zero"), R("a.frac_zero")],
3515
+ "a.nonzero": [R("a.is_zero")],
3516
  "b.exp_zero": exp_b_field, "b.exp_nzero": exp_b_field,
3517
  "b.exp_max": exp_b_field,
3518
  "b.frac_nz": frac_b_field, "b.frac_zero": frac_b_field,
3519
  "b.is_nan": [R("b.exp_max"), R("b.frac_nz")],
3520
  "b.is_inf": [R("b.exp_max"), R("b.frac_zero")],
3521
+ "b.eexp_lsb": [b(E), R("b.exp_zero")],
3522
+ "b.is_zero": [R("b.exp_zero"), R("b.frac_zero")],
3523
+ "b.nonzero": [R("b.is_zero")],
3524
  "swap.not_sel": [R("pl.lt")],
3525
  "align.not_sat": [R("align.sat")],
3526
  "align.sticky_final": [R(f"align.s{num_stages - 1}.sticky"), R("align.sat")],
 
3533
  "round.tail": [shifted(1), shifted(0), R("align.sticky_final")],
3534
  "round.rsl": [R("round.tail"), trunc(0)],
3535
  "round.up": [shifted(2), R("round.rsl")],
3536
+ "er.zero": [R(f"exp_r.fa{k}.ha2.sum.layer2") for k in range(E + 2)],
3537
+ "er.underflow": [R(f"exp_r.fa{E + 1}.ha2.sum.layer2"), R("er.zero")],
3538
+ "er.not_underflow": [R("er.underflow")],
3539
  "exp_r.zero": [R(f"exp_round.bit{k}.xor.layer2") for k in range(E + 2)],
3540
  "exp_r.underflow": [R(f"exp_round.bit{E + 1}.xor.layer2"), R("exp_r.zero")],
3541
  "exp_r.not_neg": [R(f"exp_round.bit{E + 1}.xor.layer2")],
 
3550
  "sel.not_inf_ops": [R("sel.inf_ops")],
3551
  "sel.inf_operand": [R("sel.not_nan"), R("sel.inf_ops")],
3552
  "sel.both_zero": [R("sel.not_nan"), R("sel.not_inf_ops"),
3553
+ R("a.is_zero"), R("b.is_zero")],
3554
  "sel.a_zero_pass": [R("sel.not_nan"), R("sel.not_inf_ops"),
3555
+ R("a.is_zero"), R("b.nonzero")],
3556
  "sel.b_zero_pass": [R("sel.not_nan"), R("sel.not_inf_ops"),
3557
+ R("b.is_zero"), R("a.nonzero")],
3558
  "sel.dp_reach": [R("sel.not_nan"), R("sel.not_inf_ops"),
3559
+ R("a.nonzero"), R("b.nonzero")],
3560
+ "sel.dp_zero_c": [R("lzc.not_nz")],
3561
  "sel.not_dp_zero_c": [R("sel.dp_zero_c")],
3562
  "sel.not_ovf": [R("exp_r.overflow")],
3563
  "sel.dp_zero": [R("sel.dp_reach"), R("sel.dp_zero_c")],
 
3615
  return [R("pl.le.not_gt")]
3616
 
3617
  # Swap muxes: sel = pl.lt; L side takes b when sel, S side takes a.
3618
+ m = re.match(r"^swap\.(sign_l|exp_l|exp_s|mant_l|mant_s|impl_l|impl_s)(?:\.bit(\d+))?\.(and_a|and_b|or)$", suffix)
3619
  if m:
3620
  name = m.group(1)
3621
  k = int(m.group(2)) if m.group(2) is not None else 0
 
3624
  if name == "sign_l":
3625
  a_side, b_side = a(0), b(0)
3626
  elif name in ("exp_l", "exp_s"):
3627
+ # LSB carries the subnormal correction (effective exponent >= 1).
3628
+ a_side, b_side = ((R("a.eexp_lsb"), R("b.eexp_lsb")) if k == 0
3629
+ else (a(E - k), b(E - k)))
3630
+ elif name in ("impl_l", "impl_s"):
3631
+ a_side, b_side = R("a.exp_nzero"), R("b.exp_nzero")
3632
  else:
3633
  a_side, b_side = a(W - 1 - k), b(W - 1 - k)
3634
  l_side = name.endswith("_l") or name == "sign_l"
 
3736
  bbit = int(m.group(1))
3737
  k = int(m.group(2))
3738
  kind = m.group(3)
3739
+ sel = R(f"nsh.bit{bbit}.or") # clamped shift = min(nlz, exp_L)
3740
 
3741
  def nin(kk: int) -> int:
3742
  if kk < 0 or kk >= RES:
 
3771
  k = int(m.group(1))
3772
  rest = m.group(2)
3773
  cin = R(f"rnd.bit{F - 1}.carry") if k == 0 else R(f"exp_round.bit{k - 1}.carry")
3774
+ e_bit = R(f"exprc.bit{k}") # exp_r clamped to 0 when subnormal
3775
  if rest == "xor.layer1.or" or rest == "xor.layer1.nand":
3776
  return [e_bit, cin]
3777
  if rest == "xor.layer2":
 
3783
  if m:
3784
  return [R(f"lzc.nlz.bit{int(m.group(1))}")]
3785
 
3786
+ m = re.match(r"^exprc\.bit(\d+)$", suffix) # exp_r clamped to 0 on underflow
3787
+ if m:
3788
+ k = int(m.group(1))
3789
+ return [R(f"exp_r.fa{k}.ha2.sum.layer2"), R("er.not_underflow")]
3790
+
3791
+ m = re.match(r"^nsh\.bit(\d+)\.(not_sel|and_a|and_b|or)$", suffix) # min(nlz, exp_L)
3792
+ if m:
3793
+ bbit = int(m.group(1))
3794
+ kind = m.group(2)
3795
+ if kind == "not_sel":
3796
+ return [R("er.underflow")]
3797
+ if kind == "and_a":
3798
+ return [R(f"lzc.nlz.bit{bbit}"), R(f"nsh.bit{bbit}.not_sel")]
3799
+ if kind == "and_b":
3800
+ return [exp_l(bbit), R("er.underflow")]
3801
+ return [R(f"nsh.bit{bbit}.and_a"), R(f"nsh.bit{bbit}.and_b")]
3802
+
3803
  m = re.match(r"^exp_s1\.fa(\d+)\.(.+)$", suffix)
3804
  if m:
3805
  k = int(m.group(1))
src/eval.py CHANGED
@@ -780,8 +780,9 @@ def float_div_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
780
  def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
781
  """Expected sum word under the documented contract: exact IEEE specials
782
  (NaN, infinities, opposite-sign infinities -> NaN, signed zeros, exact
783
- cancellation -> +0), flush-to-zero subnormals (a zero operand passes the
784
- other through verbatim), round-to-nearest-even mantissa."""
 
785
  E, F = exp_bits, frac_bits
786
  emax = (1 << E) - 1
787
  fmask = (1 << F) - 1
@@ -792,59 +793,72 @@ def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
792
  b_nan = eb == emax and fb != 0
793
  a_inf = ea == emax and fa == 0
794
  b_inf = eb == emax and fb == 0
795
- a_zero = ea == 0 # flush-to-zero
796
- b_zero = eb == 0
797
  if a_nan or b_nan or (a_inf and b_inf and sa != sb):
798
  return qnan
799
  if a_inf or b_inf:
800
  s = sa if a_inf else sb
801
  return (s << (E + F)) | (emax << F)
 
 
802
  if a_zero and b_zero:
803
  return (sa & sb) << (E + F)
804
  if a_zero:
805
  return bw
806
  if b_zero:
807
  return aw
808
- Ma, Mb = (1 << F) | fa, (1 << F) | fb
809
- pla, plb = (ea << F) | fa, (eb << F) | fb
 
 
 
 
810
  if pla >= plb:
811
- sL, ML, eL, MS, eS = sa, Ma, ea, Mb, eb
812
  else:
813
- sL, ML, eL, MS, eS = sb, Mb, eb, Ma, ea
814
  d = eL - eS
815
- # Exact fixed-point value with G extra low bits below the mantissa LSB,
816
- # so guard/round/sticky are all recoverable for round-to-nearest-even.
817
  G = F + 4
818
  total = (ML << (d + G)) + (MS << G) if sa == sb else (ML << (d + G)) - (MS << G)
819
  if total == 0:
820
  return 0 # exact cancellation -> +0
821
  t = total.bit_length() - 1
822
- # Normalize so the leading 1 sits at bit position (F + G); the mantissa
823
- # is the top F+1 bits, then guard/round/sticky below.
824
  exp_r = eS + (t - G) - F
825
- lead = F + G
826
- if t >= lead:
827
- sh = t - lead
828
- mant_ext = total >> sh
829
- sticky_low = 1 if (total & ((1 << sh) - 1)) else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
  else:
831
- mant_ext = total << (lead - t)
832
- sticky_low = 0
833
- frac = (mant_ext >> G) & fmask
834
- guard = (mant_ext >> (G - 1)) & 1
835
- below = (mant_ext & ((1 << (G - 1)) - 1)) or sticky_low
836
- if guard and ((frac & 1) or below): # round-to-nearest-even
837
- frac += 1
838
- if frac > fmask:
839
- frac = 0
840
- exp_r += 1
841
- inf_w = (sL << (E + F)) | (emax << F)
842
- zero_w = sL << (E + F)
843
- if exp_r >= emax:
844
- return inf_w
845
- if exp_r <= 0:
846
- return zero_w
847
- return (sL << (E + F)) | (exp_r << F) | frac
848
 
849
 
850
  # =============================================================================
 
780
  def float_add_oracle(aw: int, bw: int, exp_bits: int, frac_bits: int) -> int:
781
  """Expected sum word under the documented contract: exact IEEE specials
782
  (NaN, infinities, opposite-sign infinities -> NaN, signed zeros, exact
783
+ cancellation -> +0), subnormal operands and gradual-underflow subnormal
784
+ results, round-to-nearest-even mantissa. A zero operand passes the other
785
+ through verbatim. Pure integer arithmetic, so exact."""
786
  E, F = exp_bits, frac_bits
787
  emax = (1 << E) - 1
788
  fmask = (1 << F) - 1
 
793
  b_nan = eb == emax and fb != 0
794
  a_inf = ea == emax and fa == 0
795
  b_inf = eb == emax and fb == 0
 
 
796
  if a_nan or b_nan or (a_inf and b_inf and sa != sb):
797
  return qnan
798
  if a_inf or b_inf:
799
  s = sa if a_inf else sb
800
  return (s << (E + F)) | (emax << F)
801
+ a_zero = ea == 0 and fa == 0 # true zero, not subnormal
802
+ b_zero = eb == 0 and fb == 0
803
  if a_zero and b_zero:
804
  return (sa & sb) << (E + F)
805
  if a_zero:
806
  return bw
807
  if b_zero:
808
  return aw
809
+ Ma = ((1 << F) if ea else 0) | fa # implicit bit = (exp != 0)
810
+ Mb = ((1 << F) if eb else 0) | fb
811
+ eea = ea if ea else 1 # effective (denormal) exponent
812
+ eeb = eb if eb else 1
813
+ pla = (eea << (F + 1)) | Ma
814
+ plb = (eeb << (F + 1)) | Mb
815
  if pla >= plb:
816
+ sL, ML, eL, MS, eS = sa, Ma, eea, Mb, eeb
817
  else:
818
+ sL, ML, eL, MS, eS = sb, Mb, eeb, Ma, eea
819
  d = eL - eS
820
+ # Exact fixed-point value with G extra low bits below the mantissa LSB.
 
821
  G = F + 4
822
  total = (ML << (d + G)) + (MS << G) if sa == sb else (ML << (d + G)) - (MS << G)
823
  if total == 0:
824
  return 0 # exact cancellation -> +0
825
  t = total.bit_length() - 1
 
 
826
  exp_r = eS + (t - G) - F
827
+ if exp_r >= 1:
828
+ lead = F + G
829
+ if t >= lead:
830
+ sh = t - lead
831
+ mant_ext = total >> sh
832
+ sticky_low = 1 if (total & ((1 << sh) - 1)) else 0
833
+ else:
834
+ mant_ext = total << (lead - t)
835
+ sticky_low = 0
836
+ frac = (mant_ext >> G) & fmask
837
+ guard = (mant_ext >> (G - 1)) & 1
838
+ below = (mant_ext & ((1 << (G - 1)) - 1)) or sticky_low
839
+ if guard and ((frac & 1) or below): # round-to-nearest-even
840
+ frac += 1
841
+ if frac > fmask:
842
+ frac = 0
843
+ exp_r += 1
844
+ if exp_r >= emax:
845
+ return (sL << (E + F)) | (emax << F)
846
+ return (sL << (E + F)) | (exp_r << F) | frac
847
+ # subnormal result: gradual underflow to exponent field 0
848
+ rshift = G + 1 - eS
849
+ if rshift <= 0:
850
+ sig = total << (-rshift)
851
  else:
852
+ sig = total >> rshift
853
+ guard = (total >> (rshift - 1)) & 1
854
+ sticky = 1 if (total & ((1 << (rshift - 1)) - 1)) else 0
855
+ if guard and ((sig & 1) or sticky):
856
+ sig += 1
857
+ if sig == 0:
858
+ return sL << (E + F)
859
+ if sig >= (1 << F): # rounded up to smallest normal
860
+ return (sL << (E + F)) | (1 << F)
861
+ return (sL << (E + F)) | sig
 
 
 
 
 
 
 
862
 
863
 
864
  # =============================================================================