implement ascii and 128 bit widths

This commit is contained in:
Emil Lerch 2026-07-18 14:00:43 -07:00
parent 68823e4816
commit db20fa813e
Signed by: lobo
GPG key ID: A7B62D657EF764F8
13 changed files with 270 additions and 297 deletions

View file

@ -614,7 +614,9 @@ Fields (navigable with Up/Down):
4. HEX - editable hex (space-separated bytes: FF 00 AB CD) 4. HEX - editable hex (space-separated bytes: FF 00 AB CD)
5. OCT - editable octal 5. OCT - editable octal
6. BIN - editable binary (nibble-separated: 1111 0000) 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: Navigation:
- Up/Down moves focus between fields - Up/Down moves focus between fields
@ -623,9 +625,25 @@ Navigation:
- When bit grid is focused: left/right/up/down navigate, Space toggles - When bit grid is focused: left/right/up/down navigate, Space toggles
- Tab cycles between Standard/Programmer mode - Tab cycles between Standard/Programmer mode
- Ctrl-W cycles bit width (8/16/32/64) - 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 + Endianness:
separate input prompt at the bottom. - 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) ### 8.3 Struct Visualizer (sub-view of Programmer Mode)
``` ```

View file

@ -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.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.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.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: Programmer Mode
- **FR-2.1**: Accept input in decimal, hexadecimal (`0x`), octal (`0o`), and binary (`0b`) formats. - **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.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.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.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.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.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) ### 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`). - 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. - 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`). - 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). - 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. - Android: long-press any result to copy the raw (no-separator) value to clipboard.

View file

@ -245,15 +245,28 @@ Subcommands deferred until Phase 2 engine work is done.
- Replace passive display + input prompt with navigable editable fields - Replace passive display + input prompt with navigable editable fields
- Each base representation (DEC signed, DEC unsigned, HEX, OCT, BIN) is an - Each base representation (DEC signed, DEC unsigned, HEX, OCT, BIN) is an
editable text field 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 - 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) - 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 - Expression field at the bottom for full programmer-mode expressions
- Remove Ctrl-B keybinding (field navigation replaces it) - Remove Ctrl-B keybinding (field navigation replaces it)
- HEX field accepts/displays space-separated bytes (e.g. "FF 00 AB CD") - 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 - 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 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] ### 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 - 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) - Arrow key navigation (left/right moves cursor across bits, up/down between rows for 32/64-bit)

View file

@ -9,6 +9,7 @@ const Base = types.Base;
/// A single expression node. /// A single expression node.
pub const Expr = union(enum) { pub const Expr = union(enum) {
number: Number, number: Number,
string_literal: []const u8, // ASCII bytes to pack into integer (without quotes)
unary: Unary, unary: Unary,
binary: Binary, binary: Binary,
call: Call, call: Call,

View file

@ -64,6 +64,15 @@ pub const Environment = struct {
pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 { pub fn evaluate(env: *Environment, expr: *const Expr) CalcError!f64 {
switch (expr.*) { switch (expr.*) {
.number => |n| return n.float_value, .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| { .variable => |name| {
return env.getVar(name) orelse return CalcError.UnknownVariable; 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, // In standard mode, bitwise not doesn't really make sense,
// but we'll compute it on the integer representation // but we'll compute it on the integer representation
const int_val: u64 = @bitCast(@as(i64, @intFromFloat(operand))); 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))); return @floatFromInt(@as(i64, @bitCast(result)));
}, },
}; };
@ -335,13 +345,13 @@ test "eval tau constant" {
try testing.expectApproxEqAbs(math.tau, result, 1e-10); try testing.expectApproxEqAbs(math.tau, result, 1e-10);
} }
test "eval implicit mul: 2pi" { test "eval 2*pi" {
const result = try testEval("2pi"); const result = try testEval("2*pi");
try testing.expectApproxEqAbs(2.0 * math.pi, result, 1e-10); try testing.expectApproxEqAbs(2.0 * math.pi, result, 1e-10);
} }
test "eval implicit mul: 3(4+5)" { test "eval 3*(4+5)" {
const result = try testEval("3(4+5)"); const result = try testEval("3*(4+5)");
try testing.expectEqual(@as(f64, 27.0), result); try testing.expectEqual(@as(f64, 27.0), result);
} }

View file

@ -29,7 +29,7 @@ pub fn formatFloat(buf: []u8, value: f64) FormattedValue {
if (is_integer and @abs(value) < 1e15) { if (is_integer and @abs(value) < 1e15) {
// Format as integer with commas // 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_len = writeSignedInt(buf, int_val);
const raw = buf[0..raw_len]; 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. /// Format an integer for programmer mode hex display.
/// Display: "FF FF FF FF" (space per byte) /// Display: "FF FF FF FF" (space per byte)
/// Raw: "0xFFFFFFFF" (no separators, with prefix) /// 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 width = bit_width.bits();
const hex_digits: usize = @as(usize, width) / 4; 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 pos: usize = 2;
var i: usize = 0; var i: usize = 0;
while (i < hex_digits) : (i += 1) { 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); const nibble: u4 = @intCast((value >> shift_amt) & 0xF);
buf[pos] = hexDigit(nibble); buf[pos] = hexDigit(nibble);
pos += 1; pos += 1;
@ -82,7 +82,7 @@ pub fn formatHex(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
buf[pos] = ' '; buf[pos] = ' ';
pos += 1; 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); const nibble: u4 = @intCast((value >> shift_amt) & 0xF);
buf[pos] = hexDigit(nibble); buf[pos] = hexDigit(nibble);
pos += 1; 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. /// Format an integer for programmer mode binary display.
/// Display: "1111 0000 1010 1100" (space per nibble) /// Display: "1111 0000 1010 1100" (space per nibble)
/// Raw: "0b1111000010101100" (no separators) /// 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(); const width: usize = bit_width.bits();
// Write raw: "0b" + binary digits // 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 pos: usize = 2;
var i: usize = 0; var i: usize = 0;
while (i < width) : (i += 1) { 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); const bit: u8 = @intCast((value >> shift_amt) & 1);
buf[pos] = '0' + bit; buf[pos] = '0' + bit;
pos += 1; pos += 1;
@ -119,7 +119,7 @@ pub fn formatBinary(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
buf[pos] = ' '; buf[pos] = ' ';
pos += 1; 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); const bit: u8 = @intCast((value >> shift_amt) & 1);
buf[pos] = '0' + bit; buf[pos] = '0' + bit;
pos += 1; 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. /// Format an integer for programmer mode octal display.
/// Display: "0o37_777_777_777" (underscore per 3-digit group) /// Display: "0o37_777_777_777" (underscore per 3-digit group)
/// Raw: "0o37777777777" (no separators) /// 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 _ = bit_width; // Octal display shows all significant digits
// Write raw octal // Write raw octal
@ -147,7 +147,7 @@ pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
} }
// Calculate octal digits // 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 digit_count: usize = 0;
var v = value; var v = value;
while (v > 0) : (v /= 8) { while (v > 0) : (v /= 8) {
@ -198,10 +198,10 @@ pub fn formatOctal(buf: []u8, value: u64, bit_width: BitWidth) FormattedValue {
while (j > 0) { while (j > 0) {
j -= 1; j -= 1;
if (digits_written > 0 and digits_written == first_group) { if (digits_written > 0 and digits_written == first_group) {
buf[pos] = '_'; buf[pos] = ' ';
pos += 1; pos += 1;
} else if (digits_written > first_group and (digits_written - first_group) % 3 == 0) { } else if (digits_written > first_group and (digits_written - first_group) % 3 == 0) {
buf[pos] = '_'; buf[pos] = ' ';
pos += 1; pos += 1;
} }
buf[pos] = '0' + digits[j]; 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. /// Format an unsigned integer as decimal for programmer mode.
/// Display: "4,294,967,295" (comma-separated) /// Display: "4,294,967,295" (comma-separated)
/// Raw: "4294967295" (no separators) /// 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_len = writeUnsignedInt(buf, value);
const raw = buf[0..raw_len]; 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. /// Format a signed integer as decimal for programmer mode.
/// Display: "-1" or "4,294,967,295" /// Display: "-1" or "4,294,967,295"
/// Raw: same without commas /// 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_len = writeSignedInt(buf, value);
const raw = buf[0..raw_len]; const raw = buf[0..raw_len];
@ -248,12 +248,12 @@ fn hexDigit(nibble: u4) u8 {
return 'A' + @as(u8, nibble) - 10; return 'A' + @as(u8, nibble) - 10;
} }
fn writeUnsignedInt(buf: []u8, value: u64) usize { fn writeUnsignedInt(buf: []u8, value: u128) usize {
if (value == 0) { if (value == 0) {
buf[0] = '0'; buf[0] = '0';
return 1; return 1;
} }
var digits: [20]u8 = undefined; var digits: [39]u8 = undefined;
var count: usize = 0; var count: usize = 0;
var v = value; var v = value;
while (v > 0) : (v /= 10) { while (v > 0) : (v /= 10) {
@ -270,21 +270,21 @@ fn writeUnsignedInt(buf: []u8, value: u64) usize {
return pos; return pos;
} }
fn writeSignedInt(buf: []u8, value: i64) usize { fn writeSignedInt(buf: []u8, value: i128) usize {
if (value < 0) { if (value < 0) {
buf[0] = '-'; buf[0] = '-';
const abs_val: u64 = @intCast(-value); const abs_val: u128 = @intCast(-value);
return 1 + writeUnsignedInt(buf[1..], abs_val); return 1 + writeUnsignedInt(buf[1..], abs_val);
} }
return writeUnsignedInt(buf, @intCast(value)); return writeUnsignedInt(buf, @intCast(value));
} }
fn writeUnsignedWithCommas(buf: []u8, value: u64) usize { fn writeUnsignedWithCommas(buf: []u8, value: u128) usize {
if (value == 0) { if (value == 0) {
buf[0] = '0'; buf[0] = '0';
return 1; return 1;
} }
var digits: [20]u8 = undefined; var digits: [39]u8 = undefined;
var count: usize = 0; var count: usize = 0;
var v = value; var v = value;
while (v > 0) : (v /= 10) { while (v > 0) : (v /= 10) {
@ -308,10 +308,10 @@ fn writeUnsignedWithCommas(buf: []u8, value: u64) usize {
return pos; return pos;
} }
fn writeDecimalWithCommas(buf: []u8, value: i64) usize { fn writeDecimalWithCommas(buf: []u8, value: i128) usize {
if (value < 0) { if (value < 0) {
buf[0] = '-'; buf[0] = '-';
const abs_val: u64 = @intCast(-value); const abs_val: u128 = @intCast(-value);
return 1 + writeUnsignedWithCommas(buf[1..], abs_val); return 1 + writeUnsignedWithCommas(buf[1..], abs_val);
} }
return writeUnsignedWithCommas(buf, @intCast(value)); return writeUnsignedWithCommas(buf, @intCast(value));
@ -423,7 +423,7 @@ test "formatOctal: large with grouping" {
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
const result = formatOctal(&buf, 0xFFFF_FFFF, .bits32); const result = formatOctal(&buf, 0xFFFF_FFFF, .bits32);
// 0xFFFFFFFF = 37777777777 octal (11 digits) // 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); try testing.expectEqualStrings("0o37777777777", result.raw);
} }

View file

@ -80,20 +80,6 @@ pub const Parser = struct {
var left = try self.parsePrefix(); var left = try self.parsePrefix();
while (true) { 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); const prec = self.infixPrecedence(self.current.kind);
if (@intFromEnum(prec) <= @intFromEnum(min_prec)) break; if (@intFromEnum(prec) <= @intFromEnum(min_prec)) break;
@ -117,6 +103,14 @@ pub const Parser = struct {
.base = num.base, .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 => { .identifier => {
self.advance(); self.advance();
const name = tok.text(self.source); 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. /// Get the infix precedence of a token kind.
fn infixPrecedence(self: *Parser, kind: TokenKind) Prec { fn infixPrecedence(self: *Parser, kind: TokenKind) Prec {
return switch (kind) { return switch (kind) {
@ -360,6 +329,7 @@ fn testParseArena(source: []const u8, mode: Mode) CalcError!*Expr {
fn freeExpr(allocator: Allocator, expr: *Expr) void { fn freeExpr(allocator: Allocator, expr: *Expr) void {
switch (expr.*) { switch (expr.*) {
.number => {}, .number => {},
.string_literal => {},
.variable => {}, .variable => {},
.unary => |u| freeExpr(allocator, u.operand), .unary => |u| freeExpr(allocator, u.operand),
.binary => |b| { .binary => |b| {
@ -465,31 +435,22 @@ test "parse assignment" {
try testing.expectEqual(@as(f64, 42.0), expr.assignment.value.number.float_value); try testing.expectEqual(@as(f64, 42.0), expr.assignment.value.number.float_value);
} }
test "parse implicit mul: number identifier" { test "parse adjacent number and identifier is an error (no implicit mul)" {
// 2pi should parse as 2 * pi defer _ = test_arena_instance.reset(.retain_capacity);
const expr = try testParse("2pi", .standard); const result = testParseArena("2pi", .standard);
defer freeExpr(testing.allocator, expr); try testing.expectError(CalcError.UnexpectedToken, result);
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 implicit mul: number paren" { test "parse adjacent number and paren is an error (no implicit mul)" {
// 3(4+5) should parse as 3 * (4+5) defer _ = test_arena_instance.reset(.retain_capacity);
const expr = try testParse("3(4+5)", .standard); const result = testParseArena("3(4+5)", .standard);
defer freeExpr(testing.allocator, expr); try testing.expectError(CalcError.UnexpectedToken, result);
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 implicit mul: paren paren" { test "parse adjacent paren paren is an error (no implicit mul)" {
// (2)(3) should parse as (2) * (3) defer _ = test_arena_instance.reset(.retain_capacity);
const expr = try testParse("(2)(3)", .standard); const result = testParseArena("(2)(3)", .standard);
defer freeExpr(testing.allocator, expr); try testing.expectError(CalcError.UnexpectedToken, result);
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 caret as XOR in programmer mode" { test "parse caret as XOR in programmer mode" {

View file

@ -1,6 +1,6 @@
//! Programmer mode evaluator for Tally. //! 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. //! masked to the configured bit width. No floating-point involved.
//! Produces Integer values with signed/unsigned interpretation. //! 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. /// Recursively evaluate an expression to a raw u128.
fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u64 { fn evalExpr(config: ProgrammerConfig, expr: *const Expr) CalcError!u128 {
switch (expr.*) { switch (expr.*) {
.number => |n| { .number => |n| {
if (n.int_value) |int_val| { 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 // Float in programmer mode: truncate to integer
if (n.float_value < 0) { if (n.float_value < 0) {
const neg: i64 = @intFromFloat(n.float_value); const neg: i128 = @intFromFloat(n.float_value);
return @as(u64, @bitCast(neg)) & config.bit_width.mask(); 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(); 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 => { .variable => {
// Programmer mode doesn't support named variables (yet)
// Could add register-style variables later
return CalcError.UnknownVariable; return CalcError.UnknownVariable;
}, },
.assignment => { .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. /// Evaluate a binary operation on two u128 values, masked to bit width.
fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) CalcError!u64 { fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u128, right: u128) CalcError!u128 {
const mask = config.bit_width.mask(); const mask = config.bit_width.mask();
const width = config.bit_width.bits(); const width = config.bit_width.bits();
const result: u64 = switch (op) { const result: u128 = switch (op) {
.add => (left +% right) & mask, .add => (left +% right) & mask,
.sub => (left -% right) & mask, .sub => (left -% right) & mask,
.mul => (left *% right) & mask, .mul => (left *% right) & mask,
@ -93,7 +103,7 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) C
// Integer exponentiation // Integer exponentiation
var base = left; var base = left;
var exp = right; var exp = right;
var acc: u64 = 1; var acc: u128 = 1;
while (exp > 0) : (exp >>= 1) { while (exp > 0) : (exp >>= 1) {
if (exp & 1 != 0) acc = (acc *% base) & mask; if (exp & 1 != 0) acc = (acc *% base) & mask;
base = (base *% 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_or => left | right,
.bit_xor => left ^ right, .bit_xor => left ^ right,
.shift_left => blk: { .shift_left => blk: {
const shift_amt: u6 = if (right >= width) const shift_amt: u7 = if (right >= width)
@intCast(width - 1) @intCast(width - 1)
else else
@intCast(right); @intCast(right);
@ -112,38 +122,38 @@ fn evalBinaryOp(config: ProgrammerConfig, op: BinaryOp, left: u64, right: u64) C
}, },
.shift_right => blk: { .shift_right => blk: {
// Arithmetic right shift: preserves sign bit // Arithmetic right shift: preserves sign bit
const shift_amt: u6 = if (right >= width) const shift_amt: u7 = if (right >= width)
@intCast(width - 1) @intCast(width - 1)
else else
@intCast(right); @intCast(right);
// Sign-extend, shift, then mask // 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) { if (left & sign_bit != 0) {
// Negative: fill with 1s from the top // Negative: fill with 1s from the top
const extended = left | ~mask; 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 shifted & mask;
} }
break :blk (left >> shift_amt) & mask; break :blk (left >> shift_amt) & mask;
}, },
.shift_right_logical => blk: { .shift_right_logical => blk: {
// Logical right shift: always fills with 0s // Logical right shift: always fills with 0s
const shift_amt: u6 = if (right >= width) const shift_amt: u7 = if (right >= width)
@intCast(width - 1) @intCast(width - 1)
else else
@intCast(right); @intCast(right);
break :blk (left >> shift_amt) & mask; break :blk (left >> shift_amt) & mask;
}, },
.rotate_left => blk: { .rotate_left => blk: {
const amt: u6 = @intCast(@mod(right, width)); const amt: u7 = @intCast(@mod(right, width));
if (amt == 0) break :blk left; 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; break :blk ((left << amt) | (left >> anti)) & mask;
}, },
.rotate_right => blk: { .rotate_right => blk: {
const amt: u6 = @intCast(@mod(right, width)); const amt: u7 = @intCast(@mod(right, width));
if (amt == 0) break :blk left; 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; break :blk ((left >> amt) | (left << anti)) & mask;
}, },
}; };
@ -174,45 +184,45 @@ fn testProgWith(source: []const u8, config: ProgrammerConfig) !Integer {
test "prog: simple number" { test "prog: simple number" {
const result = try testProg("42"); 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" { test "prog: hex number" {
const result = try testProg("0xFF"); 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" { test "prog: binary number" {
const result = try testProg("0b1010"); const result = try testProg("0b1010");
try testing.expectEqual(@as(u64, 10), result.unsignedValue()); try testing.expectEqual(@as(u128, 10), result.unsignedValue());
} }
test "prog: addition" { test "prog: addition" {
const result = try testProg("10 + 20"); 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" { test "prog: subtraction wrapping" {
// 5 - 10 in 8-bit unsigned wraps // 5 - 10 in 8-bit unsigned wraps
const result = try testProgWith("5 - 10", .{ .bit_width = .bits8 }); const result = try testProgWith("5 - 10", .{ .bit_width = .bits8 });
try testing.expectEqual(@as(u64, 251), result.unsignedValue()); // 256 - 5 try testing.expectEqual(@as(u128, 251), result.unsignedValue()); // 256 - 5
try testing.expectEqual(@as(i64, -5), result.signedValue()); try testing.expectEqual(@as(i128, -5), result.signedValue());
} }
test "prog: multiplication" { test "prog: multiplication" {
const result = try testProg("6 * 7"); 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" { test "prog: multiplication overflow 8-bit" {
const result = try testProgWith("200 * 2", .{ .bit_width = .bits8 }); const result = try testProgWith("200 * 2", .{ .bit_width = .bits8 });
// 400 & 0xFF = 144 // 400 & 0xFF = 144
try testing.expectEqual(@as(u64, 144), result.unsignedValue()); try testing.expectEqual(@as(u128, 144), result.unsignedValue());
} }
test "prog: division" { test "prog: division" {
const result = try testProg("100 / 4"); 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" { test "prog: division by zero" {
@ -222,134 +232,134 @@ test "prog: division by zero" {
test "prog: modulo" { test "prog: modulo" {
const result = try testProg("10 % 3"); 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" { test "prog: power" {
const result = try testProg("2 ** 10"); 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" { test "prog: bitwise AND" {
const result = try testProg("0xFF & 0x0F"); 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" { test "prog: bitwise OR" {
const result = try testProg("0xF0 | 0x0F"); 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" { test "prog: bitwise XOR" {
const result = try testProg("0xFF ^ 0x0F"); 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" { test "prog: bitwise NOT 8-bit" {
const result = try testProgWith("~0x0F", .{ .bit_width = .bits8 }); 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" { test "prog: bitwise NOT 16-bit" {
const result = try testProgWith("~0x00FF", .{ .bit_width = .bits16 }); 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" { test "prog: bitwise NOT 32-bit" {
const result = try testProgWith("~0", .{ .bit_width = .bits32 }); 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" { test "prog: shift left" {
const result = try testProg("1 << 8"); 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" { test "prog: shift left overflow 8-bit" {
const result = try testProgWith("1 << 8", .{ .bit_width = .bits8 }); const result = try testProgWith("1 << 8", .{ .bit_width = .bits8 });
// Shifting by width or more in 8-bit: shift_amt clamped to 7 // 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" { test "prog: logical shift right" {
const result = try testProgWith("0x80 >>> 4", .{ .bit_width = .bits8 }); 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)" { test "prog: arithmetic shift right (sign bit preserved)" {
// 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64) // 0x80 in 8-bit is -128; >> 1 should give 0xC0 (-64)
const result = try testProgWith("0x80 >> 1", .{ .bit_width = .bits8 }); const result = try testProgWith("0x80 >> 1", .{ .bit_width = .bits8 });
try testing.expectEqual(@as(u64, 0xC0), result.unsignedValue()); try testing.expectEqual(@as(u128, 0xC0), result.unsignedValue());
try testing.expectEqual(@as(i64, -64), result.signedValue()); try testing.expectEqual(@as(i128, -64), result.signedValue());
} }
test "prog: arithmetic shift right (positive)" { test "prog: arithmetic shift right (positive)" {
// 0x40 in 8-bit is positive; >> 1 should give 0x20 // 0x40 in 8-bit is positive; >> 1 should give 0x20
const result = try testProgWith("0x40 >> 1", .{ .bit_width = .bits8 }); 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" { test "prog: rotate left 8-bit" {
// 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03 // 0x81 rol 1 in 8-bit: bit 7 wraps to bit 0 -> 0x03
const result = try testProgWith("0x81 rol 1", .{ .bit_width = .bits8 }); 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" { test "prog: rotate right 8-bit" {
// 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0 // 0x81 ror 1 in 8-bit: bit 0 wraps to bit 7 -> 0xC0
const result = try testProgWith("0x81 ror 1", .{ .bit_width = .bits8 }); 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" { test "prog: negation two's complement" {
const result = try testProgWith("-1", .{ .bit_width = .bits8 }); const result = try testProgWith("-1", .{ .bit_width = .bits8 });
try testing.expectEqual(@as(u64, 0xFF), result.unsignedValue()); try testing.expectEqual(@as(u128, 0xFF), result.unsignedValue());
try testing.expectEqual(@as(i64, -1), result.signedValue()); try testing.expectEqual(@as(i128, -1), result.signedValue());
} }
test "prog: negation 16-bit" { test "prog: negation 16-bit" {
const result = try testProgWith("-42", .{ .bit_width = .bits16 }); 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" { test "prog: complex expression" {
// (0xFF & 0x0F) | (1 << 4) = 0x0F | 0x10 = 0x1F // (0xFF & 0x0F) | (1 << 4) = 0x0F | 0x10 = 0x1F
const result = try testProg("(0xFF & 0x0F) | (1 << 4)"); 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" { test "prog: precedence AND before OR" {
// 0xF0 | 0xFF & 0x0F = 0xF0 | (0xFF & 0x0F) = 0xF0 | 0x0F = 0xFF // 0xF0 | 0xFF & 0x0F = 0xF0 | (0xFF & 0x0F) = 0xF0 | 0x0F = 0xFF
const result = try testProg("0xF0 | 0xFF & 0x0F"); 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" { test "prog: chained shifts" {
const result = try testProg("1 << 4 << 2"); const result = try testProg("1 << 4 << 2");
// Left-associative: (1 << 4) << 2 = 16 << 2 = 64 // 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" { test "prog: mask applied to input" {
// 0x1FF in 8-bit mode should be masked to 0xFF // 0x1FF in 8-bit mode should be masked to 0xFF
const result = try testProgWith("0x1FF", .{ .bit_width = .bits8 }); 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" { test "prog: 32-bit operations" {
const result = try testProgWith("0xFFFF_FFFF + 1", .{ .bit_width = .bits32 }); 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" { test "prog: 64-bit max" {
const result = try testProgWith("~0", .{ .bit_width = .bits64 }); 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" { test "prog: negative float input" {
// -5.0 as a float in programmer mode should become two's complement // -5.0 as a float in programmer mode should become two's complement
const result = try testProgWith("-5", .{ .bit_width = .bits8 }); 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" { test "prog: variable reference errors" {
@ -372,3 +382,31 @@ test "prog: function call errors" {
const result = evalProgrammerString(arena.allocator(), "sin(1)", .{}); const result = evalProgrammerString(arena.allocator(), "sin(1)", .{});
try testing.expectError(CalcError.UnknownFunction, result); 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);
}

View file

@ -43,24 +43,8 @@ pub const TokenKind = enum {
// Special // Special
eof, eof,
invalid, invalid,
// String literal (single-quoted, for ASCII byte packing in programmer mode)
/// Returns true if this token can be the last token before an implicit string_literal,
/// 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,
};
}
}; };
pub const Token = struct { pub const Token = struct {
@ -93,7 +77,7 @@ pub fn parseNumber(token_text: []const u8) !NumberValue {
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
var buf_len: usize = 0; var buf_len: usize = 0;
for (token_text) |c| { for (token_text) |c| {
if (c != '_' and c != ',') { if (c != '_' and c != ',' and c != ' ') {
if (buf_len >= buf.len) return error.InvalidNumber; if (buf_len >= buf.len) return error.InvalidNumber;
buf[buf_len] = c; buf[buf_len] = c;
buf_len += 1; buf_len += 1;
@ -151,51 +135,6 @@ pub fn parseNumber(token_text: []const u8) !NumberValue {
return .{ .float = f, .int_value = null, .base = .decimal }; 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 -- // -- Raw Tokenizer --
pub const Tokenizer = struct { pub const Tokenizer = struct {
@ -275,6 +214,7 @@ pub const Tokenizer = struct {
self.pos += 1; self.pos += 1;
return .{ .kind = .invalid, .start = start, .len = 1 }; return .{ .kind = .invalid, .start = start, .len = 1 };
}, },
'\'' => return self.readStringLiteral(start),
else => { else => {
self.pos += 1; self.pos += 1;
return .{ .kind = .invalid, .start = start, .len = 1 }; return .{ .kind = .invalid, .start = start, .len = 1 };
@ -303,7 +243,7 @@ pub const Tokenizer = struct {
switch (next_ch) { switch (next_ch) {
'x', 'X' => { 'x', 'X' => {
self.pos += 2; self.pos += 2;
self.consumeDigits(isHexDigit); self.consumeHexDigits();
return .{ .kind = .number, .start = start, .len = self.pos - start }; return .{ .kind = .number, .start = start, .len = self.pos - start };
}, },
'o', 'O' => { '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 { fn readIdentifier(self: *Tokenizer, start: usize) Token {
while (self.pos < self.source.len) { while (self.pos < self.source.len) {
const ch = self.source[self.pos]; const ch = self.source[self.pos];
@ -618,59 +592,10 @@ test "parseNumber with commas" {
// -- ImplicitMulStream tests -- // -- ImplicitMulStream tests --
test "implicit mul: number followed by identifier (2pi)" { test "no implicit mul: spaces are just whitespace" {
var stream = ImplicitMulStream.init("2pi", .standard); // Spaces between tokens don't create implicit multiplication
try testing.expectEqual(TokenKind.number, stream.next().kind); var tok = Tokenizer.init("2 3", .standard);
try testing.expectEqual(TokenKind.star, stream.next().kind); // synthetic try testing.expectEqual(TokenKind.number, tok.next().kind);
try testing.expectEqual(TokenKind.identifier, stream.next().kind); try testing.expectEqual(TokenKind.number, tok.next().kind);
try testing.expectEqual(TokenKind.eof, stream.next().kind); try testing.expectEqual(TokenKind.eof, tok.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);
} }

View file

@ -10,24 +10,26 @@ pub const Mode = enum {
}; };
/// Configurable integer bit width for programmer mode. /// Configurable integer bit width for programmer mode.
pub const BitWidth = enum(u7) { pub const BitWidth = enum(u8) {
bits8 = 8, bits8 = 8,
bits16 = 16, bits16 = 16,
bits32 = 32, bits32 = 32,
bits64 = 64, bits64 = 64,
bits128 = 128,
/// Returns the mask for this bit width (all bits set within width). /// 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) { return switch (self) {
.bits8 => 0xFF, .bits8 => 0xFF,
.bits16 => 0xFFFF, .bits16 => 0xFFFF,
.bits32 => 0xFFFF_FFFF, .bits32 => 0xFFFF_FFFF,
.bits64 => 0xFFFF_FFFF_FFFF_FFFF, .bits64 => 0xFFFF_FFFF_FFFF_FFFF,
.bits128 => 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF,
}; };
} }
/// Returns the number of bits as a plain integer. /// Returns the number of bits as a plain integer.
pub fn bits(self: BitWidth) u7 { pub fn bits(self: BitWidth) u8 {
return @intFromEnum(self); return @intFromEnum(self);
} }
}; };
@ -53,22 +55,22 @@ pub const Base = enum {
}; };
/// An integer value in programmer mode. /// 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 { pub const Integer = struct {
raw: u64, raw: u128,
bit_width: BitWidth, bit_width: BitWidth,
signedness: Signedness, signedness: Signedness,
/// Apply the bit width mask, truncating to the configured width. /// 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(); return self.raw & self.bit_width.mask();
} }
/// Interpret as a signed value (sign-extend from bit_width). /// 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 m = self.masked();
const width = self.bit_width.bits(); 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) { if (m & sign_bit != 0) {
// Sign extend: fill upper bits with 1s // Sign extend: fill upper bits with 1s
const extension = ~self.bit_width.mask(); const extension = ~self.bit_width.mask();
@ -78,7 +80,7 @@ pub const Integer = struct {
} }
/// Interpret as an unsigned value (just mask). /// Interpret as an unsigned value (just mask).
pub fn unsignedValue(self: Integer) u64 { pub fn unsignedValue(self: Integer) u128 {
return self.masked(); return self.masked();
} }
}; };
@ -141,32 +143,33 @@ pub const ErrorInfo = struct {
}; };
test "BitWidth.mask" { test "BitWidth.mask" {
try std.testing.expectEqual(@as(u64, 0xFF), BitWidth.bits8.mask()); try std.testing.expectEqual(@as(u128, 0xFF), BitWidth.bits8.mask());
try std.testing.expectEqual(@as(u64, 0xFFFF), BitWidth.bits16.mask()); try std.testing.expectEqual(@as(u128, 0xFFFF), BitWidth.bits16.mask());
try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF), BitWidth.bits32.mask()); try std.testing.expectEqual(@as(u128, 0xFFFF_FFFF), BitWidth.bits32.mask());
try std.testing.expectEqual(@as(u64, 0xFFFF_FFFF_FFFF_FFFF), BitWidth.bits64.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" { test "Integer.signedValue" {
// 0xFF in 8-bit signed = -1 // 0xFF in 8-bit signed = -1
const i8_neg1 = Integer{ .raw = 0xFF, .bit_width = .bits8, .signedness = .signed }; 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 // 0x7F in 8-bit signed = 127
const i8_127 = Integer{ .raw = 0x7F, .bit_width = .bits8, .signedness = .signed }; 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 // 0x80 in 8-bit signed = -128
const i8_neg128 = Integer{ .raw = 0x80, .bit_width = .bits8, .signedness = .signed }; 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 // 0xFFFF_FFFF in 32-bit signed = -1
const i32_neg1 = Integer{ .raw = 0xFFFF_FFFF, .bit_width = .bits32, .signedness = .signed }; 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" { test "Integer.unsignedValue masks correctly" {
// Extra bits beyond width are masked off // Extra bits beyond width are masked off
const val = Integer{ .raw = 0x1FF, .bit_width = .bits8, .signedness = .unsigned }; 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());
} }

View file

@ -291,11 +291,11 @@ test "evaluate: function" {
try testing.expectEqualStrings("2", result.output); try testing.expectEqualStrings("2", result.output);
} }
test "evaluate: implicit mul" { test "evaluate: explicit mul" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer _ = arena.deinit(); defer _ = arena.deinit();
var buf: [4096]u8 = undefined; 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.expect(!result.is_error);
try testing.expectEqualStrings("27", result.output); try testing.expectEqualStrings("27", result.output);
} }

View file

@ -29,9 +29,9 @@ pub const App = struct {
saved_input: ?[]const u8, saved_input: ?[]const u8,
mode: Mode, mode: Mode,
// Programmer mode state // Programmer mode state
prog_value: u64, prog_value: u128,
prog_config: engine.types.ProgrammerConfig, prog_config: engine.types.ProgrammerConfig,
bit_cursor: u6, bit_cursor: u7,
bit_grid_active: bool, bit_grid_active: bool,
pub const HistoryEntry = struct { pub const HistoryEntry = struct {
@ -114,7 +114,7 @@ pub const App = struct {
if (ans >= 0 and ans == @trunc(ans) and ans < 18446744073709551616.0) { if (ans >= 0 and ans == @trunc(ans) and ans < 18446744073709551616.0) {
self.prog_value = @intFromFloat(ans); self.prog_value = @intFromFloat(ans);
} else if (ans < 0 and ans == @trunc(ans) and ans >= -9223372036854775808.0) { } 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 = @bitCast(signed);
} }
self.prog_value &= self.prog_config.bit_width.mask(); self.prog_value &= self.prog_config.bit_width.mask();
@ -141,7 +141,8 @@ pub const App = struct {
.bits8 => .bits16, .bits8 => .bits16,
.bits16 => .bits32, .bits16 => .bits32,
.bits32 => .bits64, .bits32 => .bits64,
.bits64 => .bits8, .bits64 => .bits128,
.bits128 => .bits8,
}; };
self.prog_value &= self.prog_config.bit_width.mask(); self.prog_value &= self.prog_config.bit_width.mask();
if (self.bit_cursor >= self.prog_config.bit_width.bits()) { if (self.bit_cursor >= self.prog_config.bit_width.bits()) {
@ -229,7 +230,7 @@ pub const App = struct {
return true; return true;
} }
if (key.matches(' ', .{}) or key.matches(vaxis.Key.enter, .{})) { 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(); self.prog_value &= self.prog_config.bit_width.mask();
return true; return true;
} }

View file

@ -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 }); 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 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; const num_rows: u16 = (@as(u16, width_bits) + bits_per_row - 1) / bits_per_row;
var row = start_row; var row = start_row;