//! Programmer calculator (Windows-like): integer bases + bitwise ops. use std::fmt::Write as _; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Base { Hex, Dec, Oct, Bin, } impl Base { pub fn radix(self) -> u32 { match self { Base::Hex => 16, Base::Dec => 10, Base::Oct => 8, Base::Bin => 2, } } pub fn label(self) -> &'static str { match self { Base::Hex => "HEX", Base::Dec => "DEC", Base::Oct => "OCT", Base::Bin => "BIN", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WordSize { /// 64-bit (signed DEC). Qword, /// 32-bit unsigned (PLC DWORD). Dword, /// 32-bit signed (PLC DINT). Dint, /// 16-bit unsigned (PLC WORD). Word, /// 16-bit signed (PLC INT). Int, /// 8-bit (signed DEC). Byte, } impl WordSize { pub fn bits(self) -> u32 { match self { WordSize::Qword => 64, WordSize::Dword | WordSize::Dint => 32, WordSize::Word | WordSize::Int => 16, WordSize::Byte => 8, } } /// Signed two's-complement DEC display (INT / DINT / QWORD / BYTE). pub fn is_signed(self) -> bool { matches!( self, WordSize::Qword | WordSize::Dint | WordSize::Int | WordSize::Byte ) } pub fn label(self) -> &'static str { match self { WordSize::Qword => "QWORD", WordSize::Dword => "DWORD", WordSize::Dint => "DINT", WordSize::Word => "WORD", WordSize::Int => "INT", WordSize::Byte => "BYTE", } } pub fn from_label(s: &str) -> Option { match s.trim().to_ascii_uppercase().as_str() { "QWORD" => Some(WordSize::Qword), "DWORD" => Some(WordSize::Dword), "DINT" => Some(WordSize::Dint), "WORD" => Some(WordSize::Word), "INT" => Some(WordSize::Int), "BYTE" => Some(WordSize::Byte), _ => None, } } pub fn cycle(self) -> Self { match self { WordSize::Qword => WordSize::Dword, WordSize::Dword => WordSize::Dint, WordSize::Dint => WordSize::Word, WordSize::Word => WordSize::Int, WordSize::Int => WordSize::Byte, WordSize::Byte => WordSize::Qword, } } fn mask(self) -> u64 { match self.bits() { 64 => u64::MAX, bits => (1u64 << bits) - 1, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProgOp { Add, Sub, Mul, Div, Mod, And, Or, Xor, Nand, Nor, Lsh, Rsh, } impl ProgOp { pub fn symbol(self) -> &'static str { match self { ProgOp::Add => "+", ProgOp::Sub => "−", ProgOp::Mul => "×", ProgOp::Div => "÷", ProgOp::Mod => "%", ProgOp::And => "AND", ProgOp::Or => "OR", ProgOp::Xor => "XOR", ProgOp::Nand => "NAND", ProgOp::Nor => "NOR", ProgOp::Lsh => "Lsh", ProgOp::Rsh => "Rsh", } } fn apply(self, a: u64, b: u64, size: WordSize) -> Result { let mask = size.mask(); let a = a & mask; let b = b & mask; let r = match self { ProgOp::Add => a.wrapping_add(b), ProgOp::Sub => a.wrapping_sub(b), ProgOp::Mul => a.wrapping_mul(b), ProgOp::Div => { if b == 0 { return Err(ProgError::DivByZero); } a / b } ProgOp::Mod => { if b == 0 { return Err(ProgError::DivByZero); } a % b } ProgOp::And => a & b, ProgOp::Or => a | b, ProgOp::Xor => a ^ b, ProgOp::Nand => !(a & b), ProgOp::Nor => !(a | b), ProgOp::Lsh => { let sh = (b as u32) % size.bits().max(1); a << sh } ProgOp::Rsh => { let sh = (b as u32) % size.bits().max(1); a >> sh } }; Ok(r & mask) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProgError { DivByZero, Invalid, } impl ProgError { pub fn message(self) -> &'static str { match self { ProgError::DivByZero => "Cannot divide by zero", ProgError::Invalid => "Invalid input", } } } #[derive(Debug, Clone)] pub struct Programmer { entry: String, typing: bool, value: u64, pending: Option, op: Option, expression: String, error: Option, base: Base, word_size: WordSize, } impl Default for Programmer { fn default() -> Self { Self::new() } } impl Programmer { pub fn new() -> Self { Self { entry: "0".into(), typing: false, value: 0, pending: None, op: None, expression: String::new(), error: None, base: Base::Dec, word_size: WordSize::Qword, } } pub fn base(&self) -> Base { self.base } pub fn word_size(&self) -> WordSize { self.word_size } pub fn expression(&self) -> &str { &self.expression } pub fn display(&self) -> String { if let Some(err) = self.error { return err.message().into(); } if self.typing { format_grouped_entry(&self.entry, self.base) } else { format_value(self.value, self.base, self.word_size) } } pub fn hex_text(&self) -> String { format_value(self.current_bits(), Base::Hex, self.word_size) } pub fn dec_text(&self) -> String { format_value(self.current_bits(), Base::Dec, self.word_size) } pub fn oct_text(&self) -> String { format_value(self.current_bits(), Base::Oct, self.word_size) } pub fn bin_text(&self) -> String { format_value(self.current_bits(), Base::Bin, self.word_size) } /// Bit rows for the clickable bit board (MSB-first within each row). /// Each entry is `(is_set, bit_index)` with `bit_index` counted from LSB = 0. pub fn bit_rows(&self) -> Vec> { let n = self.word_size.bits(); let v = self.current_bits(); let cols: u32 = if n <= 8 { 8 } else { 16 }; let mut rows = Vec::new(); let mut row = Vec::new(); for i in (0..n).rev() { row.push(((v >> i) & 1 == 1, i)); if row.len() == cols as usize { rows.push(std::mem::take(&mut row)); } } if !row.is_empty() { rows.push(row); } rows } pub fn toggle_bit(&mut self, bit: u32) { if bit >= self.word_size.bits() { return; } if self.error.is_some() { self.clear_entry(); } let v = self.current_bits(); self.value = (v ^ (1u64 << bit)) & self.word_size.mask(); self.typing = false; self.error = None; self.entry = format_raw(self.value, self.base, self.word_size); } pub fn set_base(&mut self, base: Base) { if self.error.is_some() { self.clear_entry(); } // Commit typed digits into value when switching bases. if self.typing { if let Ok(v) = parse_entry(&self.entry, self.base) { self.value = v & self.word_size.mask(); } self.typing = false; } self.base = base; self.entry = format_raw(self.value, base, self.word_size); } pub fn cycle_word_size(&mut self) { if self.typing { if let Ok(v) = parse_entry(&self.entry, self.base) { self.value = v; } self.typing = false; } self.set_word_size(self.word_size.cycle()); } pub fn set_word_size(&mut self, size: WordSize) { if self.typing { if let Ok(v) = parse_entry(&self.entry, self.base) { self.value = v; } self.typing = false; } self.word_size = size; self.value &= self.word_size.mask(); if let Some(p) = self.pending.as_mut() { *p &= self.word_size.mask(); } self.entry = format_raw(self.value, self.base, self.word_size); } pub fn clear_all(&mut self) { let base = self.base; let word_size = self.word_size; *self = Self::new(); self.base = base; self.word_size = word_size; } pub fn clear_entry(&mut self) { self.error = None; self.entry = "0".into(); self.value = 0; self.typing = false; } pub fn backspace(&mut self) { if self.error.is_some() { self.clear_entry(); return; } if !self.typing { return; } self.entry.pop(); if self.entry.is_empty() { self.entry = "0".into(); self.typing = false; self.value = 0; } else if let Ok(v) = parse_entry(&self.entry, self.base) { self.value = v & self.word_size.mask(); } } pub fn input_digit(&mut self, ch: char) { let ch = ch.to_ascii_uppercase(); if !is_valid_digit(ch, self.base) { return; } if self.error.is_some() { self.clear_all(); } if !self.typing { self.entry = ch.to_string(); self.typing = true; } else if self.entry == "0" { self.entry = ch.to_string(); } else { let max_digits = max_digits(self.base, self.word_size); if digit_len(&self.entry) >= max_digits { return; } self.entry.push(ch); } match parse_entry(&self.entry, self.base) { Ok(v) => { let masked = v & self.word_size.mask(); // Reject overflow beyond word size (except wrapping interpretation of typed hex). if v != masked && self.base != Base::Bin { self.entry.pop(); if self.entry.is_empty() { self.entry = "0".into(); self.typing = false; } return; } self.value = masked; } Err(_) => { self.entry.pop(); } } } pub fn negate(&mut self) { if self.error.is_some() { return; } let v = self.current_bits(); self.value = (!v).wrapping_add(1) & self.word_size.mask(); self.typing = false; self.entry = format_raw(self.value, self.base, self.word_size); } pub fn not(&mut self) { if self.error.is_some() { return; } let v = self.current_bits(); self.value = (!v) & self.word_size.mask(); self.expression.clear(); self.typing = false; self.entry = format_raw(self.value, self.base, self.word_size); } pub fn set_op(&mut self, op: ProgOp) { if self.error.is_some() { return; } if let Err(e) = self.commit_pending() { self.fail(e); return; } let v = self.current_bits(); self.pending = Some(v); self.op = Some(op); self.expression = format!( "{} {}", format_value(v, self.base, self.word_size), op.symbol() ); self.typing = false; self.entry = format_raw(v, self.base, self.word_size); } pub fn equals(&mut self) { if self.error.is_some() { return; } let Some(op) = self.op else { self.expression.clear(); self.typing = false; return; }; let b = self.current_bits(); let a = self.pending.unwrap_or(b); match op.apply(a, b, self.word_size) { Ok(r) => { self.expression = format!( "{} {} {} =", format_value(a, self.base, self.word_size), op.symbol(), format_value(b, self.base, self.word_size) ); self.value = r; self.pending = None; self.op = None; self.typing = false; self.entry = format_raw(r, self.base, self.word_size); } Err(e) => self.fail(e), } } fn current_bits(&self) -> u64 { if self.typing { parse_entry(&self.entry, self.base) .unwrap_or(self.value) & self.word_size.mask() } else { self.value & self.word_size.mask() } } fn commit_pending(&mut self) -> Result<(), ProgError> { let (Some(a), Some(op)) = (self.pending, self.op) else { return Ok(()); }; if !self.typing { return Ok(()); } let b = self.current_bits(); let r = op.apply(a, b, self.word_size)?; self.value = r; self.pending = Some(r); self.op = None; self.entry = format_raw(r, self.base, self.word_size); Ok(()) } fn fail(&mut self, e: ProgError) { self.error = Some(e); self.pending = None; self.op = None; self.expression.clear(); self.typing = false; } } fn is_valid_digit(ch: char, base: Base) -> bool { match base { Base::Bin => matches!(ch, '0' | '1'), Base::Oct => matches!(ch, '0'..='7'), Base::Dec => ch.is_ascii_digit(), Base::Hex => ch.is_ascii_hexdigit(), } } fn digit_len(s: &str) -> usize { s.chars().filter(|c| c.is_ascii_hexdigit()).count() } fn max_digits(base: Base, size: WordSize) -> usize { let bits = size.bits() as f64; match base { Base::Bin => size.bits() as usize, Base::Oct => (bits / 3.0).ceil() as usize, Base::Dec => (bits * std::f64::consts::LN_2 / std::f64::consts::LN_10).ceil() as usize, Base::Hex => (bits / 4.0).ceil() as usize, } } fn parse_entry(entry: &str, base: Base) -> Result { let cleaned: String = entry .chars() .filter(|c| c.is_ascii_hexdigit()) .collect(); if cleaned.is_empty() { return Ok(0); } u64::from_str_radix(&cleaned, base.radix()).map_err(|_| ProgError::Invalid) } fn format_raw(v: u64, base: Base, size: WordSize) -> String { let v = v & size.mask(); match base { Base::Dec => { if size.is_signed() { format!("{}", to_signed(v, size)) } else { format!("{v}") } } Base::Hex => format!("{:X}", v), Base::Oct => format!("{:o}", v), Base::Bin => format!("{:b}", v), } } fn format_value(v: u64, base: Base, size: WordSize) -> String { format_grouped_entry(&format_raw(v, base, size), base) } fn format_grouped_entry(raw: &str, base: Base) -> String { // Keep leading minus for signed decimal. let (sign, digits) = if let Some(rest) = raw.strip_prefix('-') { ("-", rest) } else { ("", raw) }; let digits: String = digits .chars() .filter(|c| c.is_ascii_hexdigit()) .collect::() .to_ascii_uppercase(); if digits.is_empty() { return format!("{sign}0"); } let group = match base { Base::Bin => 4, Base::Hex | Base::Oct => 4, Base::Dec => 3, }; let mut out = String::new(); for (i, ch) in digits.chars().rev().enumerate() { if i > 0 && i % group == 0 { out.push(' '); } out.push(ch); } let grouped: String = out.chars().rev().collect(); let mut result = String::new(); let _ = write!(result, "{sign}{grouped}"); result } fn to_signed(v: u64, size: WordSize) -> i64 { let bits = size.bits(); if bits == 64 { return v as i64; } let mask = size.mask(); let v = v & mask; let sign_bit = 1u64 << (bits - 1); if v & sign_bit != 0 { (v | !mask) as i64 } else { v as i64 } } #[cfg(test)] mod tests { use super::*; #[test] fn hex_and() { let mut p = Programmer::new(); p.set_base(Base::Hex); p.input_digit('F'); p.input_digit('F'); p.set_op(ProgOp::And); p.input_digit('0'); p.input_digit('F'); p.equals(); assert_eq!(p.current_bits(), 0x0F); } #[test] fn simultaneous_bases() { let mut p = Programmer::new(); p.input_digit('2'); p.input_digit('5'); p.input_digit('5'); assert_eq!(p.hex_text().replace(' ', ""), "FF"); assert_eq!(p.bin_text().replace(' ', ""), "11111111"); } #[test] fn toggle_bit_flips() { let mut p = Programmer::new(); // QWORD → DWORD → DINT → WORD → INT → BYTE for _ in 0..5 { p.cycle_word_size(); } assert_eq!(p.word_size(), WordSize::Byte); p.toggle_bit(0); p.toggle_bit(7); assert_eq!(p.current_bits(), 0b1000_0001); p.toggle_bit(0); assert_eq!(p.current_bits(), 0b1000_0000); } #[test] fn int_vs_word_dec() { let mut p = Programmer::new(); p.set_base(Base::Hex); // → DWORD → DINT → WORD p.cycle_word_size(); p.cycle_word_size(); p.cycle_word_size(); assert_eq!(p.word_size(), WordSize::Word); p.input_digit('F'); p.input_digit('F'); p.input_digit('F'); p.input_digit('F'); assert_eq!(p.dec_text().replace(' ', ""), "65535"); p.cycle_word_size(); // INT assert_eq!(p.word_size(), WordSize::Int); assert_eq!(p.dec_text().replace(' ', ""), "-1"); } #[test] fn dint_vs_dword_dec() { let mut p = Programmer::new(); p.set_base(Base::Hex); p.cycle_word_size(); // DWORD assert_eq!(p.word_size(), WordSize::Dword); for _ in 0..8 { p.input_digit('F'); } assert_eq!(p.dec_text().replace(' ', ""), "4294967295"); p.cycle_word_size(); // DINT assert_eq!(p.word_size(), WordSize::Dint); assert_eq!(p.dec_text().replace(' ', ""), "-1"); } #[test] fn shift_count_mods_word_size() { // Locks hardware-style semantics: shift count is taken modulo the // word size (1 << 100000000 → 1e8 % 64 == 0 → no shift). let mut p = Programmer::new(); p.input_digit('1'); p.set_op(ProgOp::Lsh); for ch in "100000000".chars() { p.input_digit(ch); } p.equals(); assert_eq!(p.current_bits(), 1); } #[test] fn shift_out_of_word_gives_zero() { // 0xFFFF_FFFF_FFFF_FFFF Lsh 63 → only bit 0 survives at bit 63. let mut p = Programmer::new(); p.set_base(Base::Hex); for ch in "FFFFFFFFFFFFFFFF".chars() { p.input_digit(ch); } p.set_op(ProgOp::Lsh); p.set_base(Base::Dec); // type the shift count in decimal p.input_digit('6'); p.input_digit('3'); p.equals(); // (u64::MAX << 63) & u64::MAX = 0x8000_0000_0000_0000 assert_eq!(p.current_bits(), 0x8000_0000_0000_0000); } }