263 lines
9.0 KiB
Python
263 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
HP-Style RPN Calculator using Stack Language Backend
|
||
Implements classic HP calculator interface with stack display
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, font as tkfont
|
||
import sls_py
|
||
|
||
class SlsCalculator:
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("SLS Calculator")
|
||
# self.root.geometry("450x650")
|
||
self.root.resizable(False, False)
|
||
|
||
# Initialize interpreter
|
||
self.interp = sls_py.InterpreterState()
|
||
self.lexer = sls_py.LexerInfo()
|
||
|
||
# Current entry buffer
|
||
self.entry_buffer = ""
|
||
self.is_new_entry = True
|
||
|
||
# Set up UI
|
||
self.create_widgets()
|
||
|
||
def create_widgets(self):
|
||
"""Create all calculator widgets"""
|
||
# Main container
|
||
main_frame = ttk.Frame(self.root, padding="10")
|
||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # type: ignore
|
||
|
||
self.create_stack_display(main_frame)
|
||
|
||
# Entry display
|
||
self.create_entry_display(main_frame)
|
||
|
||
# Button grid
|
||
self.create_button_grid(main_frame)
|
||
|
||
def create_stack_display(self, parent):
|
||
"""Create the 4-level stack display"""
|
||
stack_frame = ttk.LabelFrame(parent, text="Stack", padding="5")
|
||
stack_frame.grid(row=0, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10)) # type: ignore
|
||
|
||
self.stack_labels = []
|
||
for i in range(4):
|
||
label = ttk.Label(stack_frame,
|
||
text=f"{3-i}:",
|
||
anchor='e',
|
||
width=30)
|
||
label.grid(row=i, column=0, sticky=(tk.W, tk.E), pady=2) # type: ignore
|
||
self.stack_labels.append(label)
|
||
|
||
def create_entry_display(self, parent):
|
||
"""Create the current entry display"""
|
||
entry_frame = ttk.Frame(parent)
|
||
entry_frame.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10)) # type: ignore
|
||
|
||
self.entry_label = ttk.Label(entry_frame,
|
||
text="0",
|
||
anchor='e',
|
||
font=('Courier', 16))
|
||
self.entry_label.grid(row=0, column=0, sticky=(tk.W, tk.E)) # type: ignore
|
||
|
||
def create_button_grid(self, parent):
|
||
"""Create the calculator button grid"""
|
||
buttons = [
|
||
[('√', self.sqrt), ('x²', self.square), ('1/x', self.reciprocal), ('NULL', lambda: None)],
|
||
[('ENTER', lambda: self.enter(True)), ('SWAP', self.swap), ('<-', self.drop), ('NULL', lambda: None)],
|
||
[('7', lambda: self.digit('7')), ('8', lambda: self.digit('8')), ('9', lambda: self.digit('9')), ('÷', self.divide)],
|
||
[('4', lambda: self.digit('4')), ('5', lambda: self.digit('5')), ('6', lambda: self.digit('6')), ('×', self.multiply)],
|
||
[('1', lambda: self.digit('1')), ('2', lambda: self.digit('2')), ('3', lambda: self.digit('3')), ('−', self.subtract)],
|
||
[('0', lambda: self.digit('0')), ('.', self.decimal), ('±', self.negate), ('+', self.add)],
|
||
]
|
||
|
||
for row_idx, row in enumerate(buttons, start=2):
|
||
for col_idx, (text, command) in enumerate(row):
|
||
if text != "NULL":
|
||
btn = ttk.Button(parent,
|
||
text=text,
|
||
command=command,
|
||
width=8)
|
||
btn.grid(row=row_idx, column=col_idx, padx=2, pady=2, sticky=(tk.W, tk.E, tk.N, tk.S)) # type: ignore
|
||
|
||
# Keyboard bindings
|
||
self.root.bind('<Delete>', lambda e: self.drop())
|
||
self.root.bind('<Return>', lambda e: self.enter(True))
|
||
self.root.bind('<KP_Enter>', lambda e: self.enter(True))
|
||
for i in range(10):
|
||
self.root.bind(str(i), lambda e, n=str(i): self.digit(n))
|
||
self.root.bind('.', lambda e: self.decimal())
|
||
self.root.bind('+', lambda e: self.add())
|
||
self.root.bind('-', lambda e: self.subtract())
|
||
self.root.bind('*', lambda e: self.multiply())
|
||
self.root.bind('/', lambda e: self.divide())
|
||
|
||
def digit(self, d):
|
||
"""Handle digit button press"""
|
||
if self.is_new_entry:
|
||
self.entry_buffer = d
|
||
self.is_new_entry = False
|
||
else:
|
||
self.entry_buffer += d
|
||
self.update_entry_display()
|
||
|
||
def decimal(self):
|
||
"""Handle decimal point"""
|
||
if self.is_new_entry:
|
||
self.entry_buffer = "0."
|
||
self.is_new_entry = False
|
||
elif '.' not in self.entry_buffer:
|
||
self.entry_buffer += '.'
|
||
self.update_entry_display()
|
||
|
||
def negate(self):
|
||
"""Toggle sign of current entry"""
|
||
if self.is_new_entry:
|
||
# Negate top of stack
|
||
self.execute_code("0 swap -")
|
||
else:
|
||
if self.entry_buffer.startswith('-'):
|
||
self.entry_buffer = self.entry_buffer[1:]
|
||
else:
|
||
self.entry_buffer = '-' + self.entry_buffer
|
||
self.update_entry_display()
|
||
|
||
def enter(self, key=False):
|
||
"""Push current entry onto stack"""
|
||
if self.entry_buffer:
|
||
self.execute_code(self.entry_buffer)
|
||
self.entry_buffer = ""
|
||
self.is_new_entry = True
|
||
elif key:
|
||
self.execute_code("dup")
|
||
self.update_displays()
|
||
|
||
def add(self):
|
||
"""Addition operation"""
|
||
self.enter()
|
||
self.execute_code("+")
|
||
self.update_displays()
|
||
|
||
def subtract(self):
|
||
"""Subtraction operation"""
|
||
self.enter()
|
||
self.execute_code("-")
|
||
self.update_displays()
|
||
|
||
def multiply(self):
|
||
"""Multiplication operation"""
|
||
self.enter()
|
||
self.execute_code("*")
|
||
self.update_displays()
|
||
|
||
def divide(self):
|
||
"""Division operation"""
|
||
self.enter()
|
||
self.execute_code("/")
|
||
self.update_displays()
|
||
|
||
def sqrt(self):
|
||
"""Square root operation"""
|
||
self.enter()
|
||
self.execute_code("sqrt")
|
||
self.update_displays()
|
||
|
||
def square(self):
|
||
"""Square operation"""
|
||
self.enter()
|
||
self.execute_code("dup *")
|
||
self.update_displays()
|
||
|
||
def reciprocal(self):
|
||
"""Reciprocal (1/x) operation"""
|
||
self.enter()
|
||
self.execute_code("1 swap /")
|
||
self.update_displays()
|
||
|
||
def swap(self):
|
||
"""Swap top two stack items"""
|
||
self.enter()
|
||
self.execute_code("swap")
|
||
self.update_displays()
|
||
|
||
def drop(self):
|
||
"""Drop top stack item"""
|
||
self.enter()
|
||
self.execute_code("drop")
|
||
self.update_displays()
|
||
|
||
def execute_code(self, code):
|
||
"""Execute stack language code"""
|
||
try:
|
||
self.lexer.source_code = code
|
||
self.lexer.pos = 0
|
||
self.lexer.column = 1
|
||
self.lexer.line = 1
|
||
|
||
tokens = sls_py.lexical_analysis(self.lexer)
|
||
|
||
for token in tokens:
|
||
if token.type == sls_py.TokenType.EOF:
|
||
break
|
||
if not self.interp.execute(token):
|
||
print(f"Error executing: {code}")
|
||
break
|
||
except Exception as e:
|
||
print(f"Exception: {e}")
|
||
|
||
def update_entry_display(self):
|
||
"""Update the entry display"""
|
||
display_text = self.entry_buffer if self.entry_buffer else "0"
|
||
self.entry_label.config(text=display_text)
|
||
|
||
def update_displays(self):
|
||
"""Update both entry and stack displays"""
|
||
self.update_entry_display()
|
||
self.update_stack_display()
|
||
|
||
def update_stack_display(self):
|
||
"""Update the 4-level stack display"""
|
||
stack_size = len(self.interp.stack)
|
||
|
||
for i in range(4):
|
||
level = 3 - i # T3, T2, T1, T0
|
||
if stack_size > level:
|
||
entry = self.interp.stack[-(level + 1)]
|
||
value_str = self.format_stack_entry(entry)
|
||
self.stack_labels[i].config(text=f"{level}: {value_str}")
|
||
else:
|
||
self.stack_labels[i].config(text=f"{level}:")
|
||
|
||
def format_stack_entry(self, entry):
|
||
"""Format a stack entry for display"""
|
||
if entry.type == sls_py.StackType.I64:
|
||
return str(entry.value)
|
||
elif entry.type == sls_py.StackType.DOUBLE:
|
||
val = entry.value
|
||
# Format with appropriate precision
|
||
if abs(val) < 1e-10 and val != 0:
|
||
return f"{val:.6e}"
|
||
elif abs(val) > 1e10:
|
||
return f"{val:.6e}"
|
||
else:
|
||
return f"{val:.10g}"
|
||
elif entry.type == sls_py.StackType.BOOLEAN:
|
||
return str(entry.value)
|
||
else:
|
||
return str(entry.value)
|
||
|
||
|
||
def main():
|
||
root = tk.Tk()
|
||
app = SlsCalculator(root)
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|