add IEEE 754 support

This commit is contained in:
Emil Lerch 2026-07-25 11:18:34 -07:00
parent ebef9c52e4
commit ae2b10a5cc
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 144 additions and 40 deletions

View file

@ -695,6 +695,11 @@ IEEE 754 float interpretation:
- Input: can type a decimal float value (e.g. `3.14`) and it
shows the closest representable bit pattern, noting if rounding
occurred
- Auto-entry: switching into programmer mode (Tab) with a non-integer
value in `ans` opens this float view automatically on the exact f64
bit pattern, since the integer bit views cannot represent a fraction.
An integer `ans` uses the integer views as usual. Infinity/NaN also
open the float view (the only place they are representable).
- Toggling bits in the grid updates the float interpretation live
- Does NOT change bit_width setting - float32 uses 32-bit view,
float64 uses 64-bit view (auto-switches width when toggling

View file

@ -280,21 +280,32 @@ Subcommands deferred until Phase 2 engine work is done.
- 128-bit width also added (u64 -> u128 throughout engine)
- Verify: `'A'` = 0x41, `'ELF'` = 0x454C46, overflow on too-long strings
### Task 5.3.3: Implement IEEE 754 float interpretation [NOT STARTED]
- Add float interpretation engine module (`engine/src/float_interp.zig`)
- Given a u32 or u64 bit pattern, decompose into sign, exponent, significand
- Compute exact decimal value (including denormals, infinities, NaN)
- Compute ULP (delta to next/previous representable value)
- Classify: normal, denormal, zero, +inf, -inf, quiet NaN, signaling NaN
- Given a decimal float, find the closest IEEE 754 bit pattern and report rounding
- TUI sub-view (Ctrl-F toggles float interpretation overlay in programmer mode):
- Color-coded bit grid (sign=pink, exponent=cyan, significand=green)
- Formula display, classification, ULP, exact value
- Typing a float value shows its bit pattern
- Auto-switch to 32-bit or 64-bit width based on selected float format
- Support float32 and float64 formats
- Verify: 0x3F800000 = 1.0 (float32), special values display correctly,
bit toggling updates float display, `3.14` shows nearest representation
### Task 5.3.3: Implement IEEE 754 float interpretation [DONE]
- Engine module `engine/src/float_interp.zig`:
- `FloatFormat` (f32/f64) with total/exponent/significand bit counts + bias
- `decompose(format, bits)` -> `FloatInfo` (sign, raw+unbiased exponent,
significand, class, value, ULP)
- Classifies normal, denormal, zero, infinity, quiet NaN, signaling NaN
- `reinterpretValue` reinterprets low bits as the float value (exact f32->f64)
- `nearestBits(format, value)` finds the closest representable pattern and
reports whether rounding occurred (detectable for f32)
- ULP computed from the exponent (2^(unbiased - significand_bits); subnormals
step by the smallest positive subnormal); NaN for inf/NaN
- 21 unit tests: 0x3F800000=1.0, special values, denormals, ULP, decimal
rounding (3.14 rounds in f32), overflow to inf, NaN handling, round trips
- TUI overlay (`src/tui/float_view.zig`, toggled with Ctrl-F in programmer mode):
- Color-coded bit grid: sign=pink, exponent=cyan, significand=green
- Shows value, classification, formula ((-1)^s x 2^e x mantissa), ULP, and
raw fields (sign, biased/unbiased exponent + bias, significand hex)
- Ctrl-W toggles f32/f64 and auto-snaps the bit width (32/64)
- Typing a decimal float (e.g. `3.14`) stores its nearest bit pattern and
notes rounding; non-float input falls back to the integer expression engine
- Auto-entry: Tab into programmer mode with a non-integer (or inf/NaN) `ans`
opens the float view on the exact f64 bit pattern; integer `ans` uses the
integer views
- Bit toggling in the grid updates the interpretation live
- Verify: `0x3F800000` shows 1.0 (f32), special values classify correctly,
`3.14` shows nearest representation with rounding note
### Task 5.4: Implement programmer mode TUI view - bit grid [DONE - merged into 5.3]
- Bit grid widget: 8 bits per group, groups separated by space

View file

@ -11,6 +11,7 @@ pub const parser = @import("parser.zig");
pub const evaluator = @import("evaluator.zig");
pub const programmer = @import("programmer.zig");
pub const formatter = @import("formatter.zig");
pub const float_interp = @import("float_interp.zig");
// Re-export primary types for convenience
pub const Value = types.Value;
@ -25,6 +26,11 @@ pub const evalStringInfo = evaluator.evalStringInfo;
pub const EvalInfo = evaluator.EvalInfo;
pub const evalProgrammerString = programmer.evalProgrammerString;
// Float interpretation
pub const FloatFormat = float_interp.FloatFormat;
pub const FloatClass = float_interp.FloatClass;
pub const FloatInfo = float_interp.FloatInfo;
test {
std.testing.refAllDecls(@This());
}

View file

@ -11,6 +11,7 @@ const vxfw = vaxis.vxfw;
const engine = @import("engine");
const draw = @import("tui/draw.zig");
const programmer_view = @import("tui/programmer.zig");
const float_view = @import("tui/float_view.zig");
const help_view = @import("tui/help.zig");
const C = draw.C;
@ -34,6 +35,9 @@ pub const App = struct {
bit_cursor: u7,
prog_field: ProgField,
value_zone_active: bool, // true = cursor in value display, false = in input
// Float interpretation overlay (programmer mode)
float_view_active: bool,
float_format: engine.FloatFormat,
pub const ProgField = enum {
bits,
@ -108,6 +112,8 @@ pub const App = struct {
.bit_cursor = 0,
.prog_field = .bits,
.value_zone_active = false,
.float_view_active = false,
.float_format = .f32,
};
}
@ -166,13 +172,29 @@ pub const App = struct {
self.env.mode = if (self.mode == .standard) .standard else .programmer;
if (self.mode == .programmer) {
const ans = self.env.ans;
if (ans >= 0 and ans == @trunc(ans) and ans < 18446744073709551616.0) {
self.prog_value = @intFromFloat(ans);
} else if (ans < 0 and ans == @trunc(ans) and ans >= -9223372036854775808.0) {
const signed: i128 = @intFromFloat(ans);
self.prog_value = @bitCast(signed);
if (ans == @trunc(ans) and ans >= -9223372036854775808.0 and ans < 18446744073709551616.0) {
// Integer value: show the integer bit views.
self.float_view_active = false;
if (ans >= 0) {
self.prog_value = @intFromFloat(ans);
} else {
const signed: i128 = @intFromFloat(ans);
self.prog_value = @bitCast(signed);
}
self.prog_value &= self.prog_config.bit_width.mask();
} else {
// Non-integer (or out-of-range / inf / NaN): the integer
// views cannot represent it, so open the float overlay on
// the exact f64 bit pattern of the value.
self.float_format = .f64;
self.float_view_active = true;
self.syncFloatWidth();
self.prog_value = @as(u64, @bitCast(ans));
self.prog_field = .bits;
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
}
}
self.prog_value &= self.prog_config.bit_width.mask();
}
self.value_zone_active = false;
ctx.redraw = true;
@ -186,22 +208,31 @@ pub const App = struct {
return;
}
// Ctrl-W: cycle bit width (programmer mode)
// Ctrl-W: cycle bit width (programmer mode). In the float overlay it
// instead toggles the float format (f32 <-> f64) and snaps the width.
if (self.mode == .programmer and key.matches('w', .{ .ctrl = true })) {
self.prog_config.bit_width = switch (self.prog_config.bit_width) {
.bits8 => .bits16,
.bits16 => .bits32,
.bits32 => .bits64,
.bits64 => .bits128,
.bits128 => .bits8,
};
// NOTE: prog_value is intentionally NOT masked here. Width is a
// display lens over the full value, so narrowing then widening
// restores the hidden upper bits. The display masks to width and
// a warning is shown while the value does not fit (see
// drawProgrammerMode). Explicit value edits still commit to width.
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
if (self.float_view_active) {
self.float_format = switch (self.float_format) {
.f32 => .f64,
.f64 => .f32,
};
self.syncFloatWidth();
} else {
self.prog_config.bit_width = switch (self.prog_config.bit_width) {
.bits8 => .bits16,
.bits16 => .bits32,
.bits32 => .bits64,
.bits64 => .bits128,
.bits128 => .bits8,
};
// NOTE: prog_value is intentionally NOT masked here. Width is a
// display lens over the full value, so narrowing then widening
// restores the hidden upper bits. The display masks to width and
// a warning is shown while the value does not fit (see
// drawProgrammerMode). Explicit value edits still commit to width.
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
}
}
ctx.redraw = true;
return;
@ -217,6 +248,21 @@ pub const App = struct {
return;
}
// Ctrl-F: toggle the IEEE 754 float interpretation overlay (programmer mode)
if (self.mode == .programmer and key.matches('f', .{ .ctrl = true })) {
self.float_view_active = !self.float_view_active;
if (self.float_view_active) {
self.syncFloatWidth();
// Keep the bit grid focused so arrows/space edit bits directly.
self.prog_field = .bits;
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
}
}
ctx.redraw = true;
return;
}
// VALUE ZONE key handling
if (self.value_zone_active) {
self.handleValueZoneKey(key);
@ -406,6 +452,17 @@ pub const App = struct {
}
}
/// Snap the bit width to match the active float format (f32 -> 32, f64 -> 64).
fn syncFloatWidth(self: *App) void {
self.prog_config.bit_width = switch (self.float_format) {
.f32 => .bits32,
.f64 => .bits64,
};
if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
self.bit_cursor = @intCast(self.prog_config.bit_width.bits() - 1);
}
}
/// How many bits the cursor moves per left/right step in the current field.
fn fieldBitStep(self: *App) u8 {
return switch (self.prog_field) {
@ -438,7 +495,11 @@ pub const App = struct {
}
if (self.mode == .programmer) {
try self.submitProgrammer(expr_text);
if (self.float_view_active) {
try self.submitFloat(expr_text);
} else {
try self.submitProgrammer(expr_text);
}
} else {
try self.submitStandard(expr_text);
}
@ -490,6 +551,22 @@ pub const App = struct {
});
}
/// In the float overlay, interpret typed input as a decimal float value and
/// store its nearest representable bit pattern. Falls back to the integer
/// expression engine for input that is not a plain float literal (e.g.
/// "0xFF" or "1 << 3").
fn submitFloat(self: *App, expr_text: []const u8) !void {
const val = std.fmt.parseFloat(f64, expr_text) catch {
try self.submitProgrammer(expr_text);
return;
};
const nearest = engine.float_interp.nearestBits(self.float_format, val);
self.prog_value = nearest.bits;
const note: []const u8 = if (nearest.rounded) " (rounded to nearest)" else "";
const result = try std.fmt.allocPrint(self.allocator, "{d}{s}", .{ val, note });
try self.history.append(self.allocator, .{ .expr = expr_text, .result = result, .is_error = false });
}
fn submitProgrammer(self: *App, expr_text: []const u8) !void {
const is_error, const display_text = if (engine.evalProgrammerString(self.allocator, expr_text, self.prog_config)) |int| blk: {
self.prog_value = int.unsignedValue();
@ -561,7 +638,11 @@ pub const App = struct {
draw.writeStr(&surface, 0, tab_col + 10, " Programmer ", prog_style);
if (self.mode == .programmer) {
programmer_view.drawProgrammerMode(self, &surface, width, height);
if (self.float_view_active) {
float_view.drawFloatView(self, &surface, width, height);
} else {
programmer_view.drawProgrammerMode(self, &surface, width, height);
}
} else {
self.drawStandardMode(&surface, width, height);
}

View file

@ -37,6 +37,7 @@ pub fn drawHelp(surface: *vxfw.Surface, width: u16, height: u16) void {
.{ "`", "Toggle input / value zone" },
.{ "Ctrl-W", "Cycle bit width (8/16/32/64/128)" },
.{ "Ctrl-E", "Toggle endianness (BE / LE)" },
.{ "Ctrl-F", "IEEE 754 float view (Ctrl-W: f32/f64)" },
.{ "Space", "Toggle bit (in grid)" },
.{ "Arrows", "Navigate fields and bits (value zone)" },
};

View file

@ -120,9 +120,9 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei
app.drawInput(surface, height -| 2);
draw.fillRow(surface, height -| 1, ' ', .{ .fg = C.muted, .bg = C.bg });
const status = if (focused == .bits)
"Arrows:nav | Space:toggle | Up/Down:field | Ctrl-W:width | Ctrl-E:endian | Tab:mode | Ctrl-C:quit"
"Arrows:nav | Space:toggle | Up/Down:field | Ctrl-W:width | Ctrl-E:endian | Ctrl-F:float | Tab:mode"
else
"Up/Down:field | Enter:set value | Ctrl-W:width | Ctrl-E:endian | Tab:mode | ?:help | Ctrl-C:quit";
"Up/Down:field | Enter:set | Ctrl-W:width | Ctrl-E:endian | Ctrl-F:float | Tab:mode | ?:help";
draw.writeStr(surface, height -| 1, 1, status, .{ .fg = C.muted, .bg = C.bg });
}