implement ascii and 128 bit widths
This commit is contained in:
parent
68823e4816
commit
db20fa813e
13 changed files with 270 additions and 297 deletions
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
11
src/tui.zig
11
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue