From ae2b10a5cc6918864b8650c340eba3fe3abf067f Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 25 Jul 2026 11:18:34 -0700 Subject: [PATCH] add IEEE 754 support --- .kiro/specs/calculator/design.md | 5 ++ .kiro/specs/calculator/tasks.md | 41 ++++++---- engine/src/engine.zig | 6 ++ src/tui.zig | 127 +++++++++++++++++++++++++------ src/tui/help.zig | 1 + src/tui/programmer.zig | 4 +- 6 files changed, 144 insertions(+), 40 deletions(-) diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index dae9693..0b6123a 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -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 diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index 2dd27f8..bb43190 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -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 diff --git a/engine/src/engine.zig b/engine/src/engine.zig index 475e874..7abd2e5 100644 --- a/engine/src/engine.zig +++ b/engine/src/engine.zig @@ -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()); } diff --git a/src/tui.zig b/src/tui.zig index c00933e..d192aa3 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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); } diff --git a/src/tui/help.zig b/src/tui/help.zig index c25bd2d..560fcc3 100644 --- a/src/tui/help.zig +++ b/src/tui/help.zig @@ -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)" }, }; diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 9d26613..75d832a 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -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 }); }