diff --git a/.kiro/specs/calculator/design.md b/.kiro/specs/calculator/design.md index fdf75ea..bfe2961 100644 --- a/.kiro/specs/calculator/design.md +++ b/.kiro/specs/calculator/design.md @@ -614,7 +614,9 @@ Fields (navigable with Up/Down): 4. HEX - editable hex (space-separated bytes: FF 00 AB CD) 5. OCT - editable octal 6. BIN - editable binary (nibble-separated: 1111 0000) -7. Expression - full expression evaluator (& | ^ ~ << >> etc.) +7. ASCII - shows printable ASCII interpretation of the bytes + (e.g., `.ELF` or `hell` - dots for non-printable) +8. Expression - full expression evaluator (& | ^ ~ << >> etc.) Navigation: - Up/Down moves focus between fields @@ -623,9 +625,25 @@ Navigation: - When bit grid is focused: left/right/up/down navigate, Space toggles - Tab cycles between Standard/Programmer mode - Ctrl-W cycles bit width (8/16/32/64) +- Ctrl-E toggles endianness display (LE/BE) -This replaces the current model where there's a passive display + -separate input prompt at the bottom. +Endianness: +- Default: little-endian (matches x86-64) +- Toggle with Ctrl-E +- Affects: HEX byte display order, ASCII interpretation order +- Does NOT affect: the underlying u64 value, DEC display, BIN display +- Header shows current setting: `Endian: LE` or `Endian: BE` + +Text/ASCII literals: +- Single-quoted strings in expressions: `'ELF'`, `'RIFF'`, `'a'` +- Each character becomes one byte packed into the integer +- Packing order follows endianness setting: + - BE: first char -> most significant used byte (`'AB'` = 0x4142) + - LE: first char -> least significant byte (`'AB'` = 0x4241) +- Max chars = bit_width / 8 (1 for 8-bit, 2 for 16, 4 for 32, 8 for 64) +- Non-ASCII characters are an error +- Useful for: magic numbers (`'ELF'`), protocol tags, inspecting + how string bytes pack into integer registers ### 8.3 Struct Visualizer (sub-view of Programmer Mode) ``` diff --git a/.kiro/specs/calculator/requirements.md b/.kiro/specs/calculator/requirements.md index d111a40..19345cb 100644 --- a/.kiro/specs/calculator/requirements.md +++ b/.kiro/specs/calculator/requirements.md @@ -17,17 +17,20 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm - **FR-1.5**: Support constants: `pi`, `e`, `tau`. - **FR-1.6**: Support variable storage (Ans for last result, named variables A-F, X, Y, Z). - **FR-1.7**: Maintain calculation history with replay capability. -- **FR-1.8**: Support implicit multiplication (e.g., `2pi`, `3(4+5)`). +- **FR-1.8**: Commas accepted as digit separators in input (e.g., `1,000 * 2` = 2000). Spaces in hex literals treated as byte separators (e.g., `0xFF FF` = 0xFFFF). ### FR-2: Programmer Mode - **FR-2.1**: Accept input in decimal, hexadecimal (`0x`), octal (`0o`), and binary (`0b`) formats. - **FR-2.2**: Simultaneously display results in all four bases (dec, hex, oct, bin). -- **FR-2.3**: Support configurable bit widths: 8, 16, 32, 64-bit. +- **FR-2.3**: Support configurable bit widths: 8, 16, 32, 64, 128-bit. - **FR-2.4**: Support bitwise operators: AND (`&`), OR (`|`), XOR (`^`), NOT (`~`), left shift (`<<`), right shift (logical `>>>`), arithmetic right shift (`>>`), rotate left (`rol`), rotate right (`ror`). - **FR-2.5**: Display both signed (two's complement) and unsigned interpretations of the current value. - **FR-2.6**: Visualize the bit pattern as a grid (integer.exposed style) - bits individually addressable/toggleable in TUI and Android. - **FR-2.7**: Quick toggle between base display formats via dedicated keyboard shortcuts (TUI). +- **FR-2.8**: Configurable endianness display (little-endian default, big-endian available). Affects byte order in HEX display and byte-level visualizations. Does not change the underlying value - purely a display/interpretation toggle. +- **FR-2.9**: ASCII/text literal input via single-quoted strings: `'hello'` packs ASCII bytes into the integer value. First character occupies the most significant used byte (big-endian packing when display is BE) or least significant byte (when display is LE). Useful for examining magic numbers, file signatures, protocol headers. +- **FR-2.10**: ASCII interpretation display - when the current value contains printable ASCII bytes, show the text representation alongside the numeric bases (e.g., `ASCII: "ELF."` or `ASCII: ..lf` with dots for non-printable bytes). ### FR-3: Struct Layout Visualizer (Programmer Mode Extension) @@ -170,7 +173,7 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm - Decimal numbers must use comma grouping for display (e.g., `4,294,967,295`). - Scientific notation only when value exceeds 15 significant digits or absolute value > 10^15 / < 10^-15. Never jump to scientific notation for values that fit in a readable decimal. -- Programmer mode hex values display with underscore grouping per 16-bit word (e.g., `0xFFFF_FFFF`). +- Programmer mode hex values display with space-separated bytes (e.g., `FF FF FF FF`). - Programmer mode binary values display grouped by nibble with spaces (e.g., `1111 1111`). - All frontends must distinguish between "display format" (with separators) and "clipboard format" (raw, no separators). - Android: long-press any result to copy the raw (no-separator) value to clipboard. diff --git a/.kiro/specs/calculator/tasks.md b/.kiro/specs/calculator/tasks.md index f894d76..c130ee1 100644 --- a/.kiro/specs/calculator/tasks.md +++ b/.kiro/specs/calculator/tasks.md @@ -245,15 +245,28 @@ Subcommands deferred until Phase 2 engine work is done. - Replace passive display + input prompt with navigable editable fields - Each base representation (DEC signed, DEC unsigned, HEX, OCT, BIN) is an editable text field -- Up/Down arrows move focus between fields (bit grid, dec, hex, oct, bin, expression) +- Up/Down arrows move focus between fields (bit grid, dec, hex, oct, bin, ascii, expression) - Typing in any field parses the value in that base and updates all other fields - Bit grid is one of the navigable fields (not a separate mode toggled with Ctrl-B) - Expression field at the bottom for full programmer-mode expressions - Remove Ctrl-B keybinding (field navigation replaces it) - HEX field accepts/displays space-separated bytes (e.g. "FF 00 AB CD") +- Add ASCII display field showing printable byte interpretation (dots for non-printable) +- Add Ctrl-E to toggle endianness (affects HEX byte order and ASCII interpretation) +- Add endianness indicator to the header line (`Endian: LE` / `Endian: BE`) - Verify: can arrow between fields, edit hex and see decimal update, toggle bits and see all fields update, evaluate expression and see result in all fields +### Task 5.3.2: Implement text/ASCII literal parsing [DONE] +- Added single-quote string token to the tokenizer (`'hello'`) +- Parser recognizes string literals as a new AST node type +- Programmer evaluator packs ASCII bytes into u128 value (BE packing: first char -> MSB) +- Standard evaluator also supports it (returns f64) +- Enforces max length = bit_width / 8 (Overflow error if too many chars) +- Rejects non-ASCII characters (> 0x7F) +- 128-bit width also added (u64 -> u128 throughout engine) +- Verify: `'A'` = 0x41, `'ELF'` = 0x454C46, overflow on too-long strings + ### 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 - Arrow key navigation (left/right moves cursor across bits, up/down between rows for 32/64-bit) diff --git a/engine/src/ast.zig b/engine/src/ast.zig index abdd298..e8ad761 100644 --- a/engine/src/ast.zig +++ b/engine/src/ast.zig @@ -9,6 +9,7 @@ const Base = types.Base; /// A single expression node. pub const Expr = union(enum) { number: Number, + string_literal: []const u8, // ASCII bytes to pack into integer (without quotes) unary: Unary, binary: Binary, call: Call, diff --git a/engine/src/evaluator.zig b/engine/src/evaluator.zig index 5766be6..c5acfc1 100644 --- a/engine/src/evaluator.zig +++ b/engine/src/evaluator.zig @@ -64,6 +64,15 @@ pub const Environment = struct { pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 { switch (expr.*) { .number => |n| return n.float_value, + .string_literal => |text| { + // Pack ASCII bytes into integer (same as programmer mode, BE packing) + var result: u128 = 0; + for (text) |byte| { + if (byte > 0x7F) return CalcError.InvalidNumber; + result = (result << 8) | byte; + } + return @floatFromInt(result); + }, .variable => |name| { return env.getVar(name) orelse return CalcError.UnknownVariable; }, @@ -80,7 +89,8 @@ pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 { // In standard mode, bitwise not doesn't really make sense, // but we'll compute it on the integer representation const int_val: u64 = @bitCast(@as(i64, @intFromFloat(operand))); - const result = ~int_val & env.programmer_config.bit_width.mask(); + const mask_val: u64 = @truncate(env.programmer_config.bit_width.mask()); + const result = ~int_val & mask_val; return @floatFromInt(@as(i64, @bitCast(result))); }, }; @@ -335,13 +345,13 @@ test "eval tau constant" { try testing.expectApproxEqAbs(math.tau, result, 1e-10); } -test "eval implicit mul: 2pi" { - const result = try testEval("2pi"); +test "eval 2*pi" { + const result = try testEval("2*pi"); try testing.expectApproxEqAbs(2.0 * math.pi, result, 1e-10); } -test "eval implicit mul: 3(4+5)" { - const result = try testEval("3(4+5)"); +test "eval 3*(4+5)" { + const result = try testEval("3*(4+5)"); try testing.expectEqual(@as(f64, 27.0), result); } diff --git a/engine/src/formatter.zig b/engine/src/formatter.zig index 394d3b5..30f0a87 100644 --- a/engine/src/formatter.zig +++ b/engine/src/formatter.zig @@ -29,7 +29,7 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue { if (is_integer and @abs(value) < 1e15) { // Format as integer with commas - const int_val: i64 = @intFromFloat(value); + const int_val: i128 = @intFromFloat(value); const raw_len = writeSignedInt(buf, int_val); const raw = buf[0..raw_len]; @@ -57,7 +57,7 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue { /// Format an integer for programmer mode hex display. /// Display: "FF FF FF FF" (space per byte) /// Raw: "0xFFFFFFFF" (no separators, with prefix) -pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { +pub fn formatHex(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { const width = bit_width.bits(); const hex_digits: usize = @as(usize, width) / 4; @@ -67,7 +67,7 @@ pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { var pos: usize = 2; var i: usize = 0; while (i < hex_digits) : (i += 1) { - const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4); + const shift_amt: u7 = @intCast((hex_digits - 1 - i) * 4); const nibble: u4 = @intCast((value >> shift_amt) & 0xF); buf[pos] = hexDigit(nibble); pos += 1; @@ -82,7 +82,7 @@ pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { buf[pos] = ' '; pos += 1; } - const shift_amt: u6 = @intCast((hex_digits - 1 - i) * 4); + const shift_amt: u7 = @intCast((hex_digits - 1 - i) * 4); const nibble: u4 = @intCast((value >> shift_amt) & 0xF); buf[pos] = hexDigit(nibble); pos += 1; @@ -95,7 +95,7 @@ pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { /// Format an integer for programmer mode binary display. /// Display: "1111 0000 1010 1100" (space per nibble) /// Raw: "0b1111000010101100" (no separators) -pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { +pub fn formatBinary(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { const width: usize = bit_width.bits(); // Write raw: "0b" + binary digits @@ -104,7 +104,7 @@ pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { var pos: usize = 2; var i: usize = 0; while (i < width) : (i += 1) { - const shift_amt: u6 = @intCast(width - 1 - i); + const shift_amt: u7 = @intCast(width - 1 - i); const bit: u8 = @intCast((value >> shift_amt) & 1); buf[pos] = '0' + bit; pos += 1; @@ -119,7 +119,7 @@ pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { buf[pos] = ' '; pos += 1; } - const shift_amt: u6 = @intCast(width - 1 - i); + const shift_amt: u7 = @intCast(width - 1 - i); const bit: u8 = @intCast((value >> shift_amt) & 1); buf[pos] = '0' + bit; pos += 1; @@ -132,7 +132,7 @@ pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { /// Format an integer for programmer mode octal display. /// Display: "0o37_777_777_777" (underscore per 3-digit group) /// Raw: "0o37777777777" (no separators) -pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { +pub fn formatOctal(buf: []u8, value: u128, bit_width: BitWidth) FormattedValue { _ = bit_width; // Octal display shows all significant digits // Write raw octal @@ -147,7 +147,7 @@ pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { } // Calculate octal digits - var digits: [22]u8 = undefined; // max 22 octal digits for u64 + var digits: [43]u8 = undefined; // max 43 octal digits for u128 var digit_count: usize = 0; var v = value; while (v > 0) : (v /= 8) { @@ -198,10 +198,10 @@ pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { while (j > 0) { j -= 1; if (digits_written > 0 and digits_written == first_group) { - buf[pos] = '_'; + buf[pos] = ' '; pos += 1; } else if (digits_written > first_group and (digits_written - first_group) % 3 == 0) { - buf[pos] = '_'; + buf[pos] = ' '; pos += 1; } buf[pos] = '0' + digits[j]; @@ -216,7 +216,7 @@ pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue { /// Format an unsigned integer as decimal for programmer mode. /// Display: "4,294,967,295" (comma-separated) /// Raw: "4294967295" (no separators) -pub fn formatDecimalUnsigned(buf: []u8, value: u64) FormattedValue { +pub fn formatDecimalUnsigned(buf: []u8, value: u128) FormattedValue { const raw_len = writeUnsignedInt(buf, value); const raw = buf[0..raw_len]; @@ -230,7 +230,7 @@ pub fn formatDecimalUnsigned(buf: []u8, value: u64) FormattedValue { /// Format a signed integer as decimal for programmer mode. /// Display: "-1" or "4,294,967,295" /// Raw: same without commas -pub fn formatDecimalSigned(buf: []u8, value: i64) FormattedValue { +pub fn formatDecimalSigned(buf: []u8, value: i128) FormattedValue { const raw_len = writeSignedInt(buf, value); const raw = buf[0..raw_len]; @@ -248,12 +248,12 @@ fn hexDigit(nibble: u4) u8 { return 'A' + @as(u8, nibble) - 10; } -fn writeUnsignedInt(buf: []u8, value: u64) usize { +fn writeUnsignedInt(buf: []u8, value: u128) usize { if (value == 0) { buf[0] = '0'; return 1; } - var digits: [20]u8 = undefined; + var digits: [39]u8 = undefined; var count: usize = 0; var v = value; while (v > 0) : (v /= 10) { @@ -270,21 +270,21 @@ fn writeUnsignedInt(buf: []u8, value: u64) usize { return pos; } -fn writeSignedInt(buf: []u8, value: i64) usize { +fn writeSignedInt(buf: []u8, value: i128) usize { if (value < 0) { buf[0] = '-'; - const abs_val: u64 = @intCast(-value); + const abs_val: u128 = @intCast(-value); return 1 + writeUnsignedInt(buf[1..], abs_val); } return writeUnsignedInt(buf, @intCast(value)); } -fn writeUnsignedWithCommas(buf: []u8, value: u64) usize { +fn writeUnsignedWithCommas(buf: []u8, value: u128) usize { if (value == 0) { buf[0] = '0'; return 1; } - var digits: [20]u8 = undefined; + var digits: [39]u8 = undefined; var count: usize = 0; var v = value; while (v > 0) : (v /= 10) { @@ -308,10 +308,10 @@ fn writeUnsignedWithCommas(buf: []u8, value: u64) usize { return pos; } -fn writeDecimalWithCommas(buf: []u8, value: i64) usize { +fn writeDecimalWithCommas(buf: []u8, value: i128) usize { if (value < 0) { buf[0] = '-'; - const abs_val: u64 = @intCast(-value); + const abs_val: u128 = @intCast(-value); return 1 + writeUnsignedWithCommas(buf[1..], abs_val); } return writeUnsignedWithCommas(buf, @intCast(value)); @@ -423,7 +423,7 @@ test "formatOctal: large with grouping" { var buf: [256]u8 = undefined; const result = formatOctal(&buf, 0xFFFF_FFFF, .bits32); // 0xFFFFFFFF = 37777777777 octal (11 digits) - try testing.expectEqualStrings("0o37_777_777_777", result.display); + try testing.expectEqualStrings("0o37 777 777 777", result.display); try testing.expectEqualStrings("0o37777777777", result.raw); } diff --git a/engine/src/parser.zig b/engine/src/parser.zig index 66750b4..cea4f70 100644 --- a/engine/src/parser.zig +++ b/engine/src/parser.zig @@ -80,20 +80,6 @@ pub const Parser = struct { var left = try self.parsePrefix(); while (true) { - // Check for implicit multiplication first: - // If current token starts a value and isn't an operator, insert mul. - if (self.isImplicitMul()) { - const mul_prec = Prec.multiplicative; - if (@intFromEnum(mul_prec) <= @intFromEnum(min_prec)) break; - const right = try self.parseExpr(mul_prec); - left = try self.makeNode(.{ .binary = .{ - .op = .mul, - .left = left, - .right = right, - } }); - continue; - } - const prec = self.infixPrecedence(self.current.kind); if (@intFromEnum(prec) <= @intFromEnum(min_prec)) break; @@ -117,6 +103,14 @@ pub const Parser = struct { .base = num.base, } }); }, + .string_literal => { + self.advance(); + const text = tok.text(self.source); + // Strip quotes: 'abc' -> abc + if (text.len < 2) return CalcError.InvalidNumber; + const content = text[1 .. text.len - 1]; + return self.makeNode(.{ .string_literal = content }); + }, .identifier => { self.advance(); const name = tok.text(self.source); @@ -249,31 +243,6 @@ pub const Parser = struct { } }); } - /// Determine if the current position represents an implicit multiplication. - /// This happens when the current token could start a new value expression - /// and the previous token ended a value expression, with no operator between. - fn isImplicitMul(self: *Parser) bool { - // Implicit mul only happens when infixPrecedence returns .none - // (meaning the current token is NOT a recognized infix operator) - // but IS the start of a value expression. - if (self.infixPrecedence(self.current.kind) != .none) return false; - - return switch (self.current.kind) { - .number, .left_paren => true, - .identifier => { - // Don't treat keyword operators as implicit mul - const name = self.current.text(self.source); - if (std.mem.eql(u8, name, "rol") or std.mem.eql(u8, name, "ror") or - std.mem.eql(u8, name, "to")) - { - return false; - } - return true; - }, - else => false, - }; - } - /// Get the infix precedence of a token kind. fn infixPrecedence(self: *Parser, kind: TokenKind) Prec { return switch (kind) { @@ -360,6 +329,7 @@ fn testParseArena(source: []const u8, mode: Mode) CalcError!*Expr { fn freeExpr(allocator: Allocator, expr: *Expr) void { switch (expr.*) { .number => {}, + .string_literal => {}, .variable => {}, .unary => |u| freeExpr(allocator, u.operand), .binary => |b| { @@ -465,31 +435,22 @@ test "parse assignment" { try testing.expectEqual(@as(f64, 42.0), expr.assignment.value.number.float_value); } -test "parse implicit mul: number identifier" { - // 2pi should parse as 2 * pi - const expr = try testParse("2pi", .standard); - defer freeExpr(testing.allocator, expr); - try testing.expectEqual(BinaryOp.mul, expr.binary.op); - try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value); - try testing.expectEqualStrings("pi", expr.binary.right.variable); +test "parse adjacent number and identifier is an error (no implicit mul)" { + defer _ = test_arena_instance.reset(.retain_capacity); + const result = testParseArena("2pi", .standard); + try testing.expectError(CalcError.UnexpectedToken, result); } -test "parse implicit mul: number paren" { - // 3(4+5) should parse as 3 * (4+5) - const expr = try testParse("3(4+5)", .standard); - defer freeExpr(testing.allocator, expr); - try testing.expectEqual(BinaryOp.mul, expr.binary.op); - try testing.expectEqual(@as(f64, 3.0), expr.binary.left.number.float_value); - try testing.expectEqual(BinaryOp.add, expr.binary.right.binary.op); +test "parse adjacent number and paren is an error (no implicit mul)" { + defer _ = test_arena_instance.reset(.retain_capacity); + const result = testParseArena("3(4+5)", .standard); + try testing.expectError(CalcError.UnexpectedToken, result); } -test "parse implicit mul: paren paren" { - // (2)(3) should parse as (2) * (3) - const expr = try testParse("(2)(3)", .standard); - defer freeExpr(testing.allocator, expr); - try testing.expectEqual(BinaryOp.mul, expr.binary.op); - try testing.expectEqual(@as(f64, 2.0), expr.binary.left.number.float_value); - try testing.expectEqual(@as(f64, 3.0), expr.binary.right.number.float_value); +test "parse adjacent paren paren is an error (no implicit mul)" { + defer _ = test_arena_instance.reset(.retain_capacity); + const result = testParseArena("(2)(3)", .standard); + try testing.expectError(CalcError.UnexpectedToken, result); } test "parse caret as XOR in programmer mode" { diff --git a/engine/src/programmer.zig b/engine/src/programmer.zig index 8943136..954c846 100644 --- a/engine/src/programmer.zig +++ b/engine/src/programmer.zig @@ -1,6 +1,6 @@ //! Programmer mode evaluator for Tally. //! -//! All operations use exact integer arithmetic (u64 storage), with results +//! All operations use exact integer arithmetic (u128 storage), with results //! masked to the configured bit width. No floating-point involved. //! Produces Integer values with signed/unsigned interpretation. @@ -26,8 +26,8 @@ pub fn evalProgrammer(config: ProgrammerConfig, expr: *const Expr) CalcError!Int }; } -/// Recursively evaluate an expression to a raw u64. -fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u64 { +/// Recursively evaluate an expression to a raw u128. +fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 { switch (expr.*) { .number => |n| { if (n.int_value) |int_val| { @@ -35,15 +35,25 @@ fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u64 { } // Float in programmer mode: truncate to integer if (n.float_value < 0) { - const neg: i64 = @intFromFloat(n.float_value); - return @as(u64, @bitCast(neg)) & config.bit_width.mask(); + const neg: i128 = @intFromFloat(n.float_value); + return @as(u128, @bitCast(neg)) & config.bit_width.mask(); } - const val: u64 = @intFromFloat(n.float_value); + const val: u128 = @intFromFloat(n.float_value); return val & config.bit_width.mask(); }, + .string_literal => |text| { + // Pack ASCII bytes into integer. + // Big-endian packing: first char -> most significant used byte. + const max_bytes = @as(usize, config.bit_width.bits()) / 8; + if (text.len > max_bytes) return CalcError.Overflow; + var result: u128 = 0; + for (text) |byte| { + if (byte > 0x7F) return CalcError.InvalidNumber; + result = (result << 8) | byte; + } + return result & config.bit_width.mask(); + }, .variable => { - // Programmer mode doesn't support named variables (yet) - // Could add register-style variables later return CalcError.UnknownVariable; }, .assignment => { @@ -72,12 +82,12 @@ fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u64 { } } -/// Evaluate a binary operation on two u64 values, masked to bit width. -fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) CalcError!u64 { +/// Evaluate a binary operation on two u128 values, masked to bit width. +fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) CalcError!u128 { const mask = config.bit_width.mask(); const width = config.bit_width.bits(); - const result: u64 = switch (op) { + const result: u128 = switch (op) { .add => (left +% right) & mask, .sub => (left -% right) & mask, .mul => (left *% right) & mask, @@ -93,7 +103,7 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) C // Integer exponentiation var base = left; var exp = right; - var acc: u64 = 1; + var acc: u128 = 1; while (exp > 0) : (exp >>= 1) { if (exp & 1 != 0) acc = (acc *% base) & mask; base = (base *% base) & mask; @@ -104,7 +114,7 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) C .bit_or => left | right, .bit_xor => left ^ right, .shift_left => blk: { - const shift_amt: u6 = if (right >= width) + const shift_amt: u7 = if (right >= width) @intCast(width - 1) else @intCast(right); @@ -112,38 +122,38 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) C }, .shift_right => blk: { // Arithmetic right shift: preserves sign bit - const shift_amt: u6 = if (right >= width) + const shift_amt: u7 = if (right >= width) @intCast(width - 1) else @intCast(right); // Sign-extend, shift, then mask - const sign_bit: u64 = @as(u64, 1) << @intCast(width - 1); + const sign_bit: u128 = @as(u128, 1) << @intCast(width - 1); if (left & sign_bit != 0) { // Negative: fill with 1s from the top const extended = left | ~mask; - const shifted: u64 = @bitCast(@as(i64, @bitCast(extended)) >> shift_amt); + const shifted: u128 = @bitCast(@as(i128, @bitCast(extended)) >> shift_amt); break :blk shifted & mask; } break :blk (left >> shift_amt) & mask; }, .shift_right_logical => blk: { // Logical right shift: always fills with 0s - const shift_amt: u6 = if (right >= width) + const shift_amt: u7 = if (right >= width) @intCast(width - 1) else @intCast(right); break :blk (left >> shift_amt) & mask; }, .rotate_left => blk: { - const amt: u6 = @intCast(@mod(right, width)); + const amt: u7 = @intCast(@mod(right, width)); if (amt == 0) break :blk left; - const anti: u6 = @intCast(width - amt); + const anti: u7 = @intCast(width - amt); break :blk ((left << amt) | (left >> anti)) & mask; }, .rotate_right => blk: { - const amt: u6 = @intCast(@mod(right, width)); + const amt: u7 = @intCast(@mod(right, width)); if (amt == 0) break :blk left; - const anti: u6 = @intCast(width - amt); + const anti: u7 = @intCast(width - amt); break :blk ((left >> amt) | (left << anti)) & mask; }, }; @@ -174,45 +184,45 @@ fn testProgWith(source: []const u8, config: ProgrammerConfig) !Integer { test "prog: simple number" { const result = try testProg("42"); - try testing.expectEqual(@as(u64, 42), result.unsignedValue()); + try testing.expectEqual(@as(u128, 42), result.unsignedValue()); } test "prog: hex number" { const result = try testProg("0xFF"); - try testing.expectEqual(@as(u64, 255), result.unsignedValue()); + try testing.expectEqual(@as(u128, 255), result.unsignedValue()); } test "prog: binary number" { const result = try testProg("0b1010"); - try testing.expectEqual(@as(u64, 10), result.unsignedValue()); + try testing.expectEqual(@as(u128, 10), result.unsignedValue()); } test "prog: addition" { const result = try testProg("10 + 20"); - try testing.expectEqual(@as(u64, 30), result.unsignedValue()); + try testing.expectEqual(@as(u128, 30), result.unsignedValue()); } test "prog: subtraction wrapping" { // 5 - 10 in 8-bit unsigned wraps const result = try testProgWith("5 - 10", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 251), result.unsignedValue()); // 256 - 5 - try testing.expectEqual(@as(i64, -5), result.signedValue()); + try testing.expectEqual(@as(u128, 251), result.unsignedValue()); // 256 - 5 + try testing.expectEqual(@as(i128, -5), result.signedValue()); } test "prog: multiplication" { const result = try testProg("6 * 7"); - try testing.expectEqual(@as(u64, 42), result.unsignedValue()); + try testing.expectEqual(@as(u128, 42), result.unsignedValue()); } test "prog: multiplication overflow 8-bit" { const result = try testProgWith("200 * 2", .{ .bit_width = .bits8 }); // 400 & 0xFF = 144 - try testing.expectEqual(@as(u64, 144), result.unsignedValue()); + try testing.expectEqual(@as(u128, 144), result.unsignedValue()); } test "prog: division" { const result = try testProg("100 / 4"); - try testing.expectEqual(@as(u64, 25), result.unsignedValue()); + try testing.expectEqual(@as(u128, 25), result.unsignedValue()); } test "prog: division by zero" { @@ -222,134 +232,134 @@ test "prog: division by zero" { test "prog: modulo" { const result = try testProg("10 % 3"); - try testing.expectEqual(@as(u64, 1), result.unsignedValue()); + try testing.expectEqual(@as(u128, 1), result.unsignedValue()); } test "prog: power" { const result = try testProg("2 ** 10"); - try testing.expectEqual(@as(u64, 1024), result.unsignedValue()); + try testing.expectEqual(@as(u128, 1024), result.unsignedValue()); } test "prog: bitwise AND" { const result = try testProg("0xFF & 0x0F"); - try testing.expectEqual(@as(u64, 0x0F), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0x0F), result.unsignedValue()); } test "prog: bitwise OR" { const result = try testProg("0xF0 | 0x0F"); - try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); } test "prog: bitwise XOR" { const result = try testProg("0xFF ^ 0x0F"); - try testing.expectEqual(@as(u64, 0xF0), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue()); } test "prog: bitwise NOT 8-bit" { const result = try testProgWith("~0x0F", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0xF0), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xF0), result.unsignedValue()); } test "prog: bitwise NOT 16-bit" { const result = try testProgWith("~0x00FF", .{ .bit_width = .bits16 }); - try testing.expectEqual(@as(u64, 0xFF00), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFF00), result.unsignedValue()); } test "prog: bitwise NOT 32-bit" { const result = try testProgWith("~0", .{ .bit_width = .bits32 }); - try testing.expectEqual(@as(u64, 0xFFFF_FFFF), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFFFF_FFFF), result.unsignedValue()); } test "prog: shift left" { const result = try testProg("1 << 8"); - try testing.expectEqual(@as(u64, 256), result.unsignedValue()); + try testing.expectEqual(@as(u128, 256), result.unsignedValue()); } test "prog: shift left overflow 8-bit" { const result = try testProgWith("1 << 8", .{ .bit_width = .bits8 }); // Shifting by width or more in 8-bit: shift_amt clamped to 7 - try testing.expectEqual(@as(u64, 128), result.unsignedValue()); + try testing.expectEqual(@as(u128, 128), result.unsignedValue()); } test "prog: logical shift right" { const result = try testProgWith("0x80 >>> 4", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0x08), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0x08), result.unsignedValue()); } test "prog: arithmetic shift right (sign bit preserved)" { // 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64) const result = try testProgWith("0x80 >> 1", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0xC0), result.unsignedValue()); - try testing.expectEqual(@as(i64, -64), result.signedValue()); + try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue()); + try testing.expectEqual(@as(i128, -64), result.signedValue()); } test "prog: arithmetic shift right (positive)" { // 0x40 in 8-bit is positive; >> 1 should give 0x20 const result = try testProgWith("0x40 >> 1", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0x20), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0x20), result.unsignedValue()); } test "prog: rotate left 8-bit" { // 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03 const result = try testProgWith("0x81 rol 1", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0x03), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0x03), result.unsignedValue()); } test "prog: rotate right 8-bit" { // 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0 const result = try testProgWith("0x81 ror 1", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0xC0), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue()); } test "prog: negation two's complement" { const result = try testProgWith("-1", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue()); - try testing.expectEqual(@as(i64, -1), result.signedValue()); + try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); + try testing.expectEqual(@as(i128, -1), result.signedValue()); } test "prog: negation 16-bit" { const result = try testProgWith("-42", .{ .bit_width = .bits16 }); - try testing.expectEqual(@as(i64, -42), result.signedValue()); + try testing.expectEqual(@as(i128, -42), result.signedValue()); } test "prog: complex expression" { // (0xFF & 0x0F) | (1 << 4) = 0x0F | 0x10 = 0x1F const result = try testProg("(0xFF & 0x0F) | (1 << 4)"); - try testing.expectEqual(@as(u64, 0x1F), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0x1F), result.unsignedValue()); } test "prog: precedence AND before OR" { // 0xF0 | 0xFF & 0x0F = 0xF0 | (0xFF & 0x0F) = 0xF0 | 0x0F = 0xFF const result = try testProg("0xF0 | 0xFF & 0x0F"); - try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); } test "prog: chained shifts" { const result = try testProg("1 << 4 << 2"); // Left-associative: (1 << 4) << 2 = 16 << 2 = 64 - try testing.expectEqual(@as(u64, 64), result.unsignedValue()); + try testing.expectEqual(@as(u128, 64), result.unsignedValue()); } test "prog: mask applied to input" { // 0x1FF in 8-bit mode should be masked to 0xFF const result = try testProgWith("0x1FF", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue()); } test "prog: 32-bit operations" { const result = try testProgWith("0xFFFF_FFFF + 1", .{ .bit_width = .bits32 }); - try testing.expectEqual(@as(u64, 0), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0), result.unsignedValue()); } test "prog: 64-bit max" { const result = try testProgWith("~0", .{ .bit_width = .bits64 }); - try testing.expectEqual(@as(u64, 0xFFFF_FFFF_FFFF_FFFF), result.unsignedValue()); + try testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), result.unsignedValue()); } test "prog: negative float input" { // -5.0 as a float in programmer mode should become two's complement const result = try testProgWith("-5", .{ .bit_width = .bits8 }); - try testing.expectEqual(@as(i64, -5), result.signedValue()); + try testing.expectEqual(@as(i128, -5), result.signedValue()); } test "prog: variable reference errors" { @@ -372,3 +382,31 @@ test "prog: function call errors" { const result = evalProgrammerString(arena.allocator(), "sin(1)", .{}); try testing.expectError(CalcError.UnknownFunction, result); } + +test "prog: ASCII literal single char" { + const result = try testProg("'A'"); + try testing.expectEqual(@as(u128, 0x41), result.unsignedValue()); +} + +test "prog: ASCII literal multi char" { + const result = try testProg("'ELF'"); + try testing.expectEqual(@as(u128, 0x454C46), result.unsignedValue()); +} + +test "prog: ASCII literal full word" { + const result = try testProg("'ascii'"); + try testing.expectEqual(@as(u128, 0x6173636969), result.unsignedValue()); +} + +test "prog: ASCII literal in expression" { + const result = try testProg("'A' | 0x20"); + // 0x41 | 0x20 = 0x61 = 'a' + try testing.expectEqual(@as(u128, 0x61), result.unsignedValue()); +} + +test "prog: ASCII literal overflow 8-bit" { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer _ = arena.deinit(); + const result = evalProgrammerString(arena.allocator(), "'AB'", .{ .bit_width = .bits8 }); + try testing.expectError(CalcError.Overflow, result); +} diff --git a/engine/src/tokenizer.zig b/engine/src/tokenizer.zig index 1493967..717cc80 100644 --- a/engine/src/tokenizer.zig +++ b/engine/src/tokenizer.zig @@ -43,24 +43,8 @@ pub const TokenKind = enum { // Special eof, invalid, - - /// Returns true if this token can be the last token before an implicit - /// multiplication (i.e. it produces a value). - pub fn isValueEnd(self: TokenKind) bool { - return switch (self) { - .number, .identifier, .right_paren => true, - else => false, - }; - } - - /// Returns true if this token can be the first token after an implicit - /// multiplication (i.e. it starts a value). - pub fn isValueStart(self: TokenKind) bool { - return switch (self) { - .number, .identifier, .left_paren, .tilde => true, - else => false, - }; - } + // String literal (single-quoted, for ASCII byte packing in programmer mode) + string_literal, }; pub const Token = struct { @@ -93,7 +77,7 @@ pub fn parseNumber(token_text: []const u8) !NumberValue { var buf: [128]u8 = undefined; var buf_len: usize = 0; for (token_text) |c| { - if (c != '_' and c != ',') { + if (c != '_' and c != ',' and c != ' ') { if (buf_len >= buf.len) return error.InvalidNumber; buf[buf_len] = c; buf_len += 1; @@ -151,51 +135,6 @@ pub fn parseNumber(token_text: []const u8) !NumberValue { return .{ .float = f, .int_value = null, .base = .decimal }; } -/// Token stream that inserts synthetic `star` tokens for implicit multiplication. -pub const ImplicitMulStream = struct { - tokenizer: Tokenizer, - source: []const u8, - buffered: ?Token, - prev_kind: TokenKind, - - pub fn init(source: []const u8, mode: Mode) ImplicitMulStream { - return .{ - .tokenizer = Tokenizer.init(source, mode), - .source = source, - .buffered = null, - .prev_kind = .eof, - }; - } - - pub fn next(self: *ImplicitMulStream) Token { - // If we have a buffered token from a previous implicit mul detection - if (self.buffered) |buf| { - self.buffered = null; - self.prev_kind = buf.kind; - return buf; - } - - const tok = self.tokenizer.next(); - - // Check for implicit multiplication: - // prev is value-end AND current is value-start - if (self.prev_kind.isValueEnd() and tok.kind.isValueStart()) { - // Buffer this token for the next call, emit star now - self.buffered = tok; - self.prev_kind = .star; - return .{ .kind = .star, .start = tok.start, .len = 0 }; - } - - self.prev_kind = tok.kind; - return tok; - } - - /// Peek at the source text for a given token. - pub fn tokenText(self: *const ImplicitMulStream, tok: Token) []const u8 { - return tok.text(self.source); - } -}; - // -- Raw Tokenizer -- pub const Tokenizer = struct { @@ -275,6 +214,7 @@ pub const Tokenizer = struct { self.pos += 1; return .{ .kind = .invalid, .start = start, .len = 1 }; }, + '\'' => return self.readStringLiteral(start), else => { self.pos += 1; return .{ .kind = .invalid, .start = start, .len = 1 }; @@ -303,7 +243,7 @@ pub const Tokenizer = struct { switch (next_ch) { 'x', 'X' => { self.pos += 2; - self.consumeDigits(isHexDigit); + self.consumeHexDigits(); return .{ .kind = .number, .start = start, .len = self.pos - start }; }, 'o', 'O' => { @@ -381,6 +321,40 @@ pub const Tokenizer = struct { } } + /// Like consumeDigits but also treats spaces as byte separators. + /// Only used for hex literals (0x...) where "FF FF FF" is valid input. + fn consumeHexDigits(self: *Tokenizer) void { + while (self.pos < self.source.len) { + const ch = self.source[self.pos]; + if (isHexDigit(ch)) { + self.pos += 1; + } else if (ch == '_') { + self.pos += 1; + } else if (ch == ' ') { + // Space in hex: only consume if followed by a hex digit + // (to avoid eating the space before an operator like "0xFF + 1") + if (self.pos + 1 < self.source.len and isHexDigit(self.source[self.pos + 1])) { + self.pos += 1; + } else { + break; + } + } else { + break; + } + } + } + + fn readStringLiteral(self: *Tokenizer, start: usize) Token { + self.pos += 1; // consume opening quote + while (self.pos < self.source.len and self.source[self.pos] != '\'') { + self.pos += 1; + } + if (self.pos < self.source.len) { + self.pos += 1; // consume closing quote + } + return .{ .kind = .string_literal, .start = start, .len = self.pos - start }; + } + fn readIdentifier(self: *Tokenizer, start: usize) Token { while (self.pos < self.source.len) { const ch = self.source[self.pos]; @@ -618,59 +592,10 @@ test "parseNumber with commas" { // -- ImplicitMulStream tests -- -test "implicit mul: number followed by identifier (2pi)" { - var stream = ImplicitMulStream.init("2pi", .standard); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic - try testing.expectEqual(TokenKind.identifier, stream.next().kind); - try testing.expectEqual(TokenKind.eof, stream.next().kind); -} - -test "implicit mul: number followed by paren 3(4+5)" { - var stream = ImplicitMulStream.init("3(4+5)", .standard); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic - try testing.expectEqual(TokenKind.left_paren, stream.next().kind); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.plus, stream.next().kind); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.right_paren, stream.next().kind); - try testing.expectEqual(TokenKind.eof, stream.next().kind); -} - -test "implicit mul: paren followed by paren )()" { - var stream = ImplicitMulStream.init("(2)(3)", .standard); - try testing.expectEqual(TokenKind.left_paren, stream.next().kind); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.right_paren, stream.next().kind); - try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic - try testing.expectEqual(TokenKind.left_paren, stream.next().kind); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.right_paren, stream.next().kind); - try testing.expectEqual(TokenKind.eof, stream.next().kind); -} - -test "implicit mul: identifier followed by paren (function call) does NOT insert star" { - // "sin(x)" - identifier followed by ( is a function call, not implicit mul. - // However, at the tokenizer level, identifier IS a value-end and ( IS a value-start, - // so the ImplicitMulStream WILL insert a star. The PARSER is responsible for - // recognizing "identifier * (" as a function call pattern (when the star is - // synthetic/zero-length) and handling it correctly. - // - // This test documents the actual stream behavior: - var stream = ImplicitMulStream.init("sin(x)", .standard); - try testing.expectEqual(TokenKind.identifier, stream.next().kind); - try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic - try testing.expectEqual(TokenKind.left_paren, stream.next().kind); - try testing.expectEqual(TokenKind.identifier, stream.next().kind); - try testing.expectEqual(TokenKind.right_paren, stream.next().kind); - try testing.expectEqual(TokenKind.eof, stream.next().kind); -} - -test "no implicit mul: number + operator + number" { - var stream = ImplicitMulStream.init("2 + 3", .standard); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.plus, stream.next().kind); - try testing.expectEqual(TokenKind.number, stream.next().kind); - try testing.expectEqual(TokenKind.eof, stream.next().kind); +test "no implicit mul: spaces are just whitespace" { + // Spaces between tokens don't create implicit multiplication + var tok = Tokenizer.init("2 3", .standard); + try testing.expectEqual(TokenKind.number, tok.next().kind); + try testing.expectEqual(TokenKind.number, tok.next().kind); + try testing.expectEqual(TokenKind.eof, tok.next().kind); } diff --git a/engine/src/types.zig b/engine/src/types.zig index b4f34bc..d930b1f 100644 --- a/engine/src/types.zig +++ b/engine/src/types.zig @@ -10,24 +10,26 @@ pub const Mode = enum { }; /// Configurable integer bit width for programmer mode. -pub const BitWidth = enum(u7) { +pub const BitWidth = enum(u8) { bits8 = 8, bits16 = 16, bits32 = 32, bits64 = 64, + bits128 = 128, /// Returns the mask for this bit width (all bits set within width). - pub fn mask(self: BitWidth) u64 { + pub fn mask(self: BitWidth) u128 { return switch (self) { .bits8 => 0xFF, .bits16 => 0xFFFF, .bits32 => 0xFFFF_FFFF, .bits64 => 0xFFFF_FFFF_FFFF_FFFF, + .bits128 => 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF, }; } /// Returns the number of bits as a plain integer. - pub fn bits(self: BitWidth) u7 { + pub fn bits(self: BitWidth) u8 { return @intFromEnum(self); } }; @@ -53,22 +55,22 @@ pub const Base = enum { }; /// An integer value in programmer mode. -/// Raw bits stored in u64; interpretation depends on bit_width and signedness. +/// Raw bits stored in u128; interpretation depends on bit_width and signedness. pub const Integer = struct { - raw: u64, + raw: u128, bit_width: BitWidth, signedness: Signedness, /// Apply the bit width mask, truncating to the configured width. - pub fn masked(self: Integer) u64 { + pub fn masked(self: Integer) u128 { return self.raw & self.bit_width.mask(); } /// Interpret as a signed value (sign-extend from bit_width). - pub fn signedValue(self: Integer) i64 { + pub fn signedValue(self: Integer) i128 { const m = self.masked(); const width = self.bit_width.bits(); - const sign_bit: u64 = @as(u64, 1) << @intCast(width - 1); + const sign_bit: u128 = @as(u128, 1) << @intCast(width - 1); if (m & sign_bit != 0) { // Sign extend: fill upper bits with 1s const extension = ~self.bit_width.mask(); @@ -78,7 +80,7 @@ pub const Integer = struct { } /// Interpret as an unsigned value (just mask). - pub fn unsignedValue(self: Integer) u64 { + pub fn unsignedValue(self: Integer) u128 { return self.masked(); } }; @@ -141,32 +143,33 @@ pub const ErrorInfo = struct { }; test "BitWidth.mask" { - try std.testing.expectEqual(@as(u64, 0xFF), BitWidth.bits8.mask()); - try std.testing.expectEqual(@as(u64, 0xFFFF), BitWidth.bits16.mask()); - try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF), BitWidth.bits32.mask()); - try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask()); + try std.testing.expectEqual(@as(u128, 0xFF), BitWidth.bits8.mask()); + try std.testing.expectEqual(@as(u128, 0xFFFF), BitWidth.bits16.mask()); + try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF), BitWidth.bits32.mask()); + try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.mask()); + try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF), BitWidth.bits128.mask()); } test "Integer.signedValue" { // 0xFF in 8-bit signed = -1 const i8_neg1 = Integer{ .raw = 0xFF, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i64, -1), i8_neg1.signedValue()); + try std.testing.expectEqual(@as(i128, -1), i8_neg1.signedValue()); // 0x7F in 8-bit signed = 127 const i8_127 = Integer{ .raw = 0x7F, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i64, 127), i8_127.signedValue()); + try std.testing.expectEqual(@as(i128, 127), i8_127.signedValue()); // 0x80 in 8-bit signed = -128 const i8_neg128 = Integer{ .raw = 0x80, .bit_width = .bits8, .signedness = .signed }; - try std.testing.expectEqual(@as(i64, -128), i8_neg128.signedValue()); + try std.testing.expectEqual(@as(i128, -128), i8_neg128.signedValue()); // 0xFFFF_FFFF in 32-bit signed = -1 const i32_neg1 = Integer{ .raw = 0xFFFF_FFFF, .bit_width = .bits32, .signedness = .signed }; - try std.testing.expectEqual(@as(i64, -1), i32_neg1.signedValue()); + try std.testing.expectEqual(@as(i128, -1), i32_neg1.signedValue()); } test "Integer.unsignedValue masks correctly" { // Extra bits beyond width are masked off const val = Integer{ .raw = 0x1FF, .bit_width = .bits8, .signedness = .unsigned }; - try std.testing.expectEqual(@as(u64, 0xFF), val.unsignedValue()); + try std.testing.expectEqual(@as(u128, 0xFF), val.unsignedValue()); } diff --git a/src/main.zig b/src/main.zig index 1172f75..5d36554 100644 --- a/src/main.zig +++ b/src/main.zig @@ -291,11 +291,11 @@ test "evaluate: function" { try testing.expectEqualStrings("2", result.output); } -test "evaluate: implicit mul" { +test "evaluate: explicit mul" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; - const result = evaluate(arena.allocator(), "3(4+5)", .standard, &buf); + const result = evaluate(arena.allocator(), "3*(4+5)", .standard, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("27", result.output); } diff --git a/src/tui.zig b/src/tui.zig index d8c5982..734d2aa 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -29,9 +29,9 @@ pub const App = struct { saved_input: ?[]const u8, mode: Mode, // Programmer mode state - prog_value: u64, + prog_value: u128, prog_config: engine.types.ProgrammerConfig, - bit_cursor: u6, + bit_cursor: u7, bit_grid_active: bool, pub const HistoryEntry = struct { @@ -114,7 +114,7 @@ pub const App = struct { 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: i64 = @intFromFloat(ans); + const signed: i128 = @intFromFloat(ans); self.prog_value = @bitCast(signed); } self.prog_value &= self.prog_config.bit_width.mask(); @@ -141,7 +141,8 @@ pub const App = struct { .bits8 => .bits16, .bits16 => .bits32, .bits32 => .bits64, - .bits64 => .bits8, + .bits64 => .bits128, + .bits128 => .bits8, }; self.prog_value &= self.prog_config.bit_width.mask(); if (self.bit_cursor >= self.prog_config.bit_width.bits()) { @@ -229,7 +230,7 @@ pub const App = struct { return true; } if (key.matches(' ', .{}) or key.matches(vaxis.Key.enter, .{})) { - self.prog_value ^= @as(u64, 1) << self.bit_cursor; + self.prog_value ^= @as(u128, 1) << self.bit_cursor; self.prog_value &= self.prog_config.bit_width.mask(); return true; } diff --git a/src/tui/programmer.zig b/src/tui/programmer.zig index 92f56be..5bdfea4 100644 --- a/src/tui/programmer.zig +++ b/src/tui/programmer.zig @@ -72,9 +72,9 @@ pub fn drawProgrammerMode(app: *tui.App, surface: *vxfw.Surface, width: u16, hei draw.writeStr(surface, height -| 1, 1, status, .{ .fg = C.muted, .bg = C.bg }); } -fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u64, bw: engine.types.BitWidth) void { +fn drawBitGrid(app: *tui.App, surface: *vxfw.Surface, start_row: u16, val: u128, bw: engine.types.BitWidth) void { const width_bits = bw.bits(); - const bits_per_row: u7 = if (width_bits > 32) 32 else width_bits; + const bits_per_row: u8 = if (width_bits > 32) 32 else width_bits; const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row; var row = start_row;