xrpn 2.4 → 2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84914aee77f72d7498f7550ebd2baab36750512ef40df1821a5eecd3e990bc42
4
- data.tar.gz: 05f5f0827b724fc60f627ecf7a830396ae037c6e2cfaf5f0fe3240fb519a4e58
3
+ metadata.gz: 0451ad69d90e115070029b6619c12eeea1c3470044080d656b4e7130ff5a7ce2
4
+ data.tar.gz: ac6027e1bf40f409fb5be3fa75ab7dbadecc934ec7a5a3165c6dd82d21b94bd0
5
5
  SHA512:
6
- metadata.gz: 644a84de681db279ebc4d710559c05b7d5411733474ea40b8f7ee54644176d3c371c8dbc86cffac9d89a05ea5549a77cae382289731f2ef441f16415d305d3c2
7
- data.tar.gz: 7a573988b18edbed9c245eeb82c2b3fc3038375f72ba2832ebe1d2f5114c182723ef122d5ca99cb8193477a5f9b9b6fc46388a60cb51454214f812f94f74323d
6
+ metadata.gz: 8a67e64e4e2e63fcc712c21d8520a726e753a8978ada1e3c7b56f52cf8611faa63b351df020251ed784ccc402d95ae3413186c20396dcc9325bee966ae548ab6
7
+ data.tar.gz: 6621b4407c422896602022105fc38e159b247ee6856b263fea01366a30c1f1bbeb1951df5c449cc24c999df483b19ff8ae348503fce4cddd4f836e4df1f55ccb
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # XRPN
2
- ![Ruby](https://img.shields.io/badge/language-Ruby-red) [![Gem Version](https://badge.fury.io/rb/xrpn.svg)](https://badge.fury.io/rb/xrpn) ![Unlicense](https://img.shields.io/badge/license-Unlicense-green) ![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-important)
3
2
 
4
- ## Introduction
3
+ ![Ruby](https://img.shields.io/badge/language-Ruby-red) [![Gem Version](https://badge.fury.io/rb/xrpn.svg)](https://badge.fury.io/rb/xrpn) ![Unlicense](https://img.shields.io/badge/license-Unlicense-green) ![Stay Amazing](https://img.shields.io/badge/Stay-Amazing-important)
5
4
 
6
- XRPN is a stack-based programming language, similar to [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)), but simpler in nature.
5
+ <img src="img/xrpn_logo.svg" align="left" width="150" height="150"> XRPN is a stack-based programming language, similar to [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)), but simpler in nature.
6
+ <br clear="left"/>
7
7
 
8
8
  XRPN is on-the-fly extensible. Language functions can be upgraded or implemented while programs are running.
9
9
 
@@ -79,6 +79,50 @@ AVIEW
79
79
  END
80
80
  ```
81
81
 
82
+ ## HP-41 RAW File Support
83
+
84
+ XRPN can now inspect HP-41 RAW format program files. Use the `RAWINFO` command to view labels, strings, and hex dump of RAW files.
85
+
86
+ **Usage:**
87
+ ```
88
+ xrpn
89
+ > "/path/to/program.raw"
90
+ > rawinfo
91
+ ```
92
+
93
+ This displays:
94
+ - All program labels (global and local)
95
+ - Text strings found in the program
96
+ - Hex dump for analysis
97
+
98
+ **Note:** Full RAW-to-XRPN conversion requires comprehensive HP-41 bytecode documentation and is planned for future releases. The current implementation provides informational viewing of RAW files.
99
+
100
+ ## Changelog
101
+
102
+ ### Version 2.6 (Latest)
103
+ **New Features and Testing**
104
+
105
+ - **Added HP-41 RAW file viewer** - New `RAWINFO` command displays labels, strings, and hex dump from HP-41 RAW program files
106
+ - **Comprehensive regression test framework** - 58 automated tests across 7 command categories with 100% pass rate
107
+ - **Test infrastructure** - YAML-based test specifications for easy maintenance and extension
108
+
109
+ ### Version 2.5
110
+ **Critical Bug Fixes and Performance Enhancements**
111
+
112
+ - **Fixed division by zero crashes** - Now properly handles division by zero with error messages instead of crashing
113
+ - **Fixed nil reference errors in statistics** - Statistics commands (like `mean`) now handle empty or uninitialized registers gracefully
114
+ - **Fixed variable name error in file operations** - Corrected undefined variable reference in `getfile` command
115
+ - **Fixed indirect addressing nil references** - Indirect addressing now handles nil register values safely
116
+ - **Optimized file system operations** - Improved startup performance by using `FileUtils.mkdir_p` for directory creation
117
+ - **Fixed memory leak** - TTY::Prompt is now lazily initialized only when debug mode is actually used
118
+ - **Optimized numeric formatting** - Eliminated redundant string conversions in rounding overflow calculations
119
+ - **Added comprehensive test suite** - New test programs in `/tests/` directory to verify bug fixes and prevent regressions
120
+
121
+ This release significantly improves the stability and performance of XRPN while maintaining full backward compatibility.
122
+
123
+ ### Version 2.4
124
+ - Fixed loading of files via the -f switch
125
+
82
126
  ## Documentation
83
127
 
84
128
  ...is all in [the wiki page in this repo](https://github.com/isene/xrpn/wiki/XRPN-Documentation).
data/bin/xrpn CHANGED
@@ -27,9 +27,11 @@ require 'timeout'
27
27
 
28
28
 
29
29
  # ENSURE DIRs
30
- Dir.mkdir(Dir.home + "/.xrpn") unless File.exist?(Dir.home + "/.xrpn")
30
+ require 'fileutils'
31
+ home_xrpn = File.join(Dir.home, ".xrpn")
32
+ FileUtils.mkdir_p(home_xrpn)
31
33
  ["data", "extra", "print", "xcmd", "xlib"].each do |d|
32
- Dir.mkdir(Dir.home + "/.xrpn/" + d) unless File.exist?(Dir.home + "/.xrpn/" + d)
34
+ FileUtils.mkdir_p(File.join(home_xrpn, d))
33
35
  end
34
36
 
35
37
  # READ LIBRARY AND COMMANDS
@@ -102,7 +104,7 @@ load(Dir.home+'/.xrpn/theme') if File.exist?(Dir.home+'/.xrpn/theme') # Override
102
104
  # THE CORE OF THE RPN PROGRAM
103
105
  @nl = false # Nolift set to false
104
106
  @i = 0 # Counter to prevent infinite loops
105
- @debugprompt = TTY::Prompt.new
107
+ @debugprompt = nil
106
108
  until @p.pc == @p.prg[@p.pg].length do
107
109
  # Enter debug mode if $debug or $prompt is set, else read next program line
108
110
  ($debug or $prompt) ? debug_mode : @line = @p.prg[@p.pg][@p.pc]
data/tests/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # XRPN Regression Test Framework
2
+
3
+ Comprehensive automated testing suite for XRPN to prevent regressions and ensure all commands work correctly.
4
+
5
+ ## Quick Start
6
+
7
+ Run all tests:
8
+ ```bash
9
+ ruby tests/run_tests.rb
10
+ ```
11
+
12
+ Run specific test category:
13
+ ```bash
14
+ ruby tests/run_tests.rb tests/specs/01_basic_arithmetic.yml
15
+ ```
16
+
17
+ ## Test Framework Architecture
18
+
19
+ ### Components
20
+
21
+ **run_tests.rb** - Main test runner
22
+ - Orchestrates test execution
23
+ - Parses YAML test specifications
24
+ - Validates outputs
25
+ - Reports results
26
+
27
+ **specs/** - Test specification directory
28
+ - YAML files defining test cases
29
+ - Organized by command category
30
+ - Easy to read and extend
31
+
32
+ ### Test Specification Format
33
+
34
+ Tests are defined in YAML files in `specs/` directory:
35
+
36
+ ```yaml
37
+ ---
38
+ - name: "Addition: 5 + 3 = 8"
39
+ commands:
40
+ - "5"
41
+ - "3"
42
+ - "+"
43
+ expected:
44
+ x: 8
45
+
46
+ - name: "With setup"
47
+ setup:
48
+ - "deg" # Setup commands run first
49
+ commands:
50
+ - "30"
51
+ - "sin"
52
+ expected:
53
+ x: 0.5
54
+ ```
55
+
56
+ ### Test Fields
57
+
58
+ **name** (required) - Descriptive test name
59
+
60
+ **commands** (required) - Array of XRPN commands to execute
61
+
62
+ **setup** (optional) - Array of commands to run before test
63
+
64
+ **expected** (required) - Expected results
65
+ - `x`: Expected X register value (number)
66
+ - `tolerance`: Acceptable error margin (default: 0.0001)
67
+ - `contains`: String that should appear in output
68
+
69
+ **should_error** (optional) - Set to `true` if test should produce error
70
+
71
+ ### Example Tests
72
+
73
+ Basic arithmetic:
74
+ ```yaml
75
+ - name: "Square root: sqrt(144) = 12"
76
+ commands:
77
+ - "144"
78
+ - "sqrt"
79
+ expected:
80
+ x: 12
81
+ ```
82
+
83
+ With setup:
84
+ ```yaml
85
+ - name: "sin(30°) = 0.5"
86
+ setup:
87
+ - "deg"
88
+ commands:
89
+ - "30"
90
+ - "sin"
91
+ expected:
92
+ x: 0.5
93
+ ```
94
+
95
+ With tolerance:
96
+ ```yaml
97
+ - name: "Pi approximation"
98
+ commands:
99
+ - "pi"
100
+ expected:
101
+ x: 3.14159
102
+ tolerance: 0.00001
103
+ ```
104
+
105
+ Error handling:
106
+ ```yaml
107
+ - name: "Statistics with empty registers"
108
+ setup:
109
+ - "clrg"
110
+ commands:
111
+ - "mean"
112
+ should_error: true
113
+ ```
114
+
115
+ ## Test Categories
116
+
117
+ Current test suites:
118
+
119
+ 1. **01_basic_arithmetic.yml** - Math operations (+, -, *, /, sqrt, pow, etc.)
120
+ 2. **02_stack_operations.yml** - Stack manipulation (enter, swap, drop, etc.)
121
+ 3. **03_trigonometry.yml** - Trig functions (sin, cos, tan, etc.)
122
+ 4. **04_logarithms.yml** - Log and exponential functions
123
+ 5. **05_registers.yml** - Register operations (sto, rcl, indirect, etc.)
124
+ 6. **06_alpha.yml** - Alpha register and string operations
125
+ 7. **07_statistics.yml** - Statistical functions (mean, sdev, Σ+, etc.)
126
+ 8. **08_conditionals.yml** - Conditional tests (x=0?, x>y?, etc.)
127
+ 9. **09_base_conversion.yml** - Number base conversions
128
+ 10. **10_flags.yml** - Flag operations (sf, cf, fs?, fc?)
129
+
130
+ ## Adding New Tests
131
+
132
+ ### Create New Test Category
133
+
134
+ 1. Create new YAML file in `tests/specs/`:
135
+ ```bash
136
+ vi tests/specs/11_my_category.yml
137
+ ```
138
+
139
+ 2. Add test cases:
140
+ ```yaml
141
+ ---
142
+ # My Category Tests
143
+ - name: "My first test"
144
+ commands:
145
+ - "cmd1"
146
+ - "cmd2"
147
+ expected:
148
+ x: 42
149
+ ```
150
+
151
+ 3. Run tests:
152
+ ```bash
153
+ ruby tests/run_tests.rb
154
+ ```
155
+
156
+ ### Add Test to Existing Category
157
+
158
+ Edit the appropriate YAML file in `tests/specs/` and add your test case following the format above.
159
+
160
+ ## How It Works
161
+
162
+ 1. **Test Execution**
163
+ - Reads YAML test specifications
164
+ - Builds command strings with setup + commands + stdout + off
165
+ - Pipes commands to xrpn: `echo "cmd1,cmd2,stdout,off" | xrpn`
166
+ - Captures first line of output (the stdout result)
167
+
168
+ 2. **Validation**
169
+ - Parses numeric output from stdout
170
+ - Compares against expected value within tolerance
171
+ - Reports pass/fail for each test
172
+
173
+ 3. **Output**
174
+ - Clear pass (✓) / fail (✗) indicators
175
+ - Expected vs actual values for failures
176
+ - Summary with total/passed/failed counts
177
+
178
+ ## Best Practices
179
+
180
+ ### Test Naming
181
+ - Be descriptive: "sin(30°) = 0.5" not "test sin"
182
+ - Include operation and expected result
183
+ - Use standard math notation
184
+
185
+ ### Test Organization
186
+ - Group related commands in same file
187
+ - Use setup for common initialization
188
+ - One test per specific behavior
189
+
190
+ ### Tolerances
191
+ - Default 0.0001 works for most cases
192
+ - Increase for transcendental functions: `tolerance: 0.00001`
193
+ - Use exact match for integers (default tolerance fine)
194
+
195
+ ### Edge Cases
196
+ - Test boundary conditions
197
+ - Test error handling with `should_error: true`
198
+ - Test indirect addressing
199
+ - Test register overflow/underflow
200
+
201
+ ## Extending the Framework
202
+
203
+ ### Custom Validators
204
+
205
+ Edit `run_tests.rb` to add custom validation logic:
206
+
207
+ ```ruby
208
+ def validate_output(output, expected, test)
209
+ # Add custom validation here
210
+ end
211
+ ```
212
+
213
+ ### Output Formats
214
+
215
+ Currently validates X register via stdout.
216
+ Future: Add alpha, register, flag validation.
217
+
218
+ ### Continuous Integration
219
+
220
+ Add to CI pipeline:
221
+ ```bash
222
+ #!/bin/bash
223
+ cd xrpn
224
+ ruby tests/run_tests.rb
225
+ exit $?
226
+ ```
227
+
228
+ ## Troubleshooting
229
+
230
+ **Tests hang/timeout:**
231
+ - Ensure all test programs include implicit "off" (added automatically)
232
+ - Check for infinite loops in test commands
233
+
234
+ **Numeric comparison failures:**
235
+ - Increase tolerance if needed
236
+ - Check European vs US decimal format (handled automatically)
237
+
238
+ **Command not found:**
239
+ - Verify command exists in `/xcmd/` or `/xlib/`
240
+ - Check command spelling in YAML
241
+
242
+ ## Contributing Tests
243
+
244
+ When adding new XRPN features:
245
+
246
+ 1. Write tests FIRST (TDD approach)
247
+ 2. Run tests to see them fail
248
+ 3. Implement feature
249
+ 4. Run tests to see them pass
250
+ 5. Commit both feature and tests together
251
+
252
+ This ensures:
253
+ - Features work as intended
254
+ - Future changes don't break existing functionality
255
+ - Documentation via executable examples
256
+
257
+ ## License
258
+
259
+ Same as XRPN - Public Domain (Unlicense)
@@ -0,0 +1,44 @@
1
+ LBL "FINAL"
2
+ "Final bug test"
3
+ aview
4
+ PSE
5
+
6
+ "Testing division..."
7
+ aview
8
+ 10
9
+ 2
10
+ /
11
+ "10/2 = "
12
+ arcl x
13
+ aview
14
+ PSE
15
+
16
+ "Testing stats..."
17
+ aview
18
+ clrg
19
+ 1
20
+ 2
21
+ Σ+
22
+ 3
23
+ 4
24
+ Σ+
25
+ mean
26
+ "Mean: "
27
+ arcl x
28
+ aview
29
+ PSE
30
+
31
+ "Testing file error..."
32
+ aview
33
+ "nothere.txt"
34
+ asto 01
35
+ rcl 01
36
+ getfile
37
+ "File result: "
38
+ arcl x
39
+ aview
40
+ PSE
41
+
42
+ "All tests passed!"
43
+ aview
44
+ end
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ #
4
+ # XRPN Regression Test Framework
5
+ # Tests all xrpn commands to prevent regressions
6
+ # Usage: ruby tests/run_tests.rb [test_file.yml]
7
+
8
+ require 'yaml'
9
+ require 'json'
10
+ require 'fileutils'
11
+
12
+ class XRPNTestRunner
13
+ attr_reader :passed, :failed, :errors
14
+
15
+ def initialize
16
+ @passed = 0
17
+ @failed = 0
18
+ @errors = 0
19
+ @test_results = []
20
+ @xrpn_bin = File.expand_path('../bin/xrpn', __dir__)
21
+ end
22
+
23
+ def run_all_tests
24
+ test_dir = File.dirname(__FILE__)
25
+ test_files = Dir["#{test_dir}/specs/*.yml"].sort
26
+
27
+ if test_files.empty?
28
+ puts "No test files found in #{test_dir}/specs/"
29
+ puts "Creating test specification directory..."
30
+ FileUtils.mkdir_p("#{test_dir}/specs")
31
+ return
32
+ end
33
+
34
+ puts "=" * 60
35
+ puts "XRPN Regression Test Suite"
36
+ puts "=" * 60
37
+ puts
38
+
39
+ test_files.each do |file|
40
+ run_test_file(file)
41
+ end
42
+
43
+ print_summary
44
+ end
45
+
46
+ def run_test_file(file)
47
+ tests = YAML.load_file(file)
48
+ category = File.basename(file, '.yml')
49
+
50
+ puts "Testing: #{category}"
51
+ puts "-" * 60
52
+
53
+ tests.each do |test|
54
+ run_single_test(test, category)
55
+ end
56
+ puts
57
+ end
58
+
59
+ def run_single_test(test, category)
60
+ name = test['name']
61
+ commands = test['commands']
62
+ expected = test['expected'] || {}
63
+ setup = test['setup'] || []
64
+
65
+ # Build XRPN program as comma-separated commands
66
+ program = build_program(setup, commands, expected)
67
+
68
+ # Write to temp file to avoid shell escaping issues
69
+ temp_file = "/tmp/xrpn_test_#{Process.pid}_#{rand(10000)}.txt"
70
+ File.write(temp_file, program)
71
+
72
+ # Run xrpn by piping commands and capture output
73
+ output = `cat #{temp_file} | #{@xrpn_bin} 2>&1`
74
+ exit_code = $?.exitstatus
75
+
76
+ # Clean up
77
+ File.delete(temp_file) if File.exist?(temp_file)
78
+
79
+ # Parse output and validate
80
+ result = validate_output(output, expected, test)
81
+
82
+ if result[:passed]
83
+ @passed += 1
84
+ print " ✓ #{name}"
85
+ puts " (#{category})"
86
+ else
87
+ @failed += 1
88
+ print " ✗ #{name}"
89
+ puts " (#{category})"
90
+ puts " Expected: #{result[:expected]}"
91
+ puts " Got: #{result[:actual]}"
92
+ puts " Output: #{output.strip}" if test['verbose']
93
+ end
94
+
95
+ rescue => e
96
+ @errors += 1
97
+ puts " ✗ #{name} - ERROR: #{e.message}"
98
+ end
99
+
100
+ def build_program(setup, commands, expected)
101
+ lines = []
102
+
103
+ # Setup phase
104
+ setup.each { |cmd| lines << cmd }
105
+
106
+ # Test commands
107
+ commands.each { |cmd| lines << cmd }
108
+
109
+ # If testing alpha, move alpha to X register before stdout
110
+ lines << "asto x" if expected['alpha']
111
+
112
+ # Output results for validation using stdout
113
+ lines << "stdout" # Output X to stdout for capture
114
+ lines << "off" # Exit cleanly
115
+
116
+ lines.join(",") # Join with commas
117
+ end
118
+
119
+ def validate_output(output, expected, test)
120
+ result = { passed: true, expected: {}, actual: {} }
121
+
122
+ # Check for error conditions
123
+ if test['should_error']
124
+ if output.include?("Error") || output.include?("ERROR") || output.include?("error")
125
+ return result
126
+ else
127
+ result[:passed] = false
128
+ result[:expected] = "Error condition"
129
+ result[:actual] = "No error"
130
+ return result
131
+ end
132
+ end
133
+
134
+ # Parse X register from stdout output (just a number on first line)
135
+ if expected['x']
136
+ # Get first line and convert European decimal format to standard
137
+ first_line = output.split("\n").first.to_s.strip
138
+ clean_output = first_line.gsub(',', '.')
139
+ actual_x = clean_output.to_f rescue nil
140
+ expected_x = expected['x'].to_f
141
+
142
+ tolerance = expected['tolerance'] || 0.0001
143
+ if actual_x.nil? || (actual_x - expected_x).abs > tolerance
144
+ result[:passed] = false
145
+ result[:expected]['x'] = expected_x
146
+ result[:actual]['x'] = actual_x
147
+ end
148
+ end
149
+
150
+ # Parse Alpha register from stdout output (string on first line)
151
+ if expected['alpha']
152
+ first_line = output.split("\n").first.to_s.strip
153
+ actual_alpha = first_line
154
+ expected_alpha = expected['alpha']
155
+
156
+ if actual_alpha != expected_alpha
157
+ result[:passed] = false
158
+ result[:expected]['alpha'] = expected_alpha
159
+ result[:actual]['alpha'] = actual_alpha
160
+ end
161
+ end
162
+
163
+ # Parse string in X register (for commands like dechex that put strings in X)
164
+ if expected['x_string']
165
+ first_line = output.split("\n").first.to_s.strip
166
+ actual_string = first_line
167
+ expected_string = expected['x_string']
168
+
169
+ if actual_string != expected_string
170
+ result[:passed] = false
171
+ result[:expected]['x_string'] = expected_string
172
+ result[:actual]['x_string'] = actual_string
173
+ end
174
+ end
175
+
176
+ # Check for specific output patterns
177
+ if expected['contains']
178
+ unless output.include?(expected['contains'])
179
+ result[:passed] = false
180
+ result[:expected]['output'] = "Contains '#{expected['contains']}'"
181
+ result[:actual]['output'] = "Not found"
182
+ end
183
+ end
184
+
185
+ result
186
+ end
187
+
188
+ def print_summary
189
+ total = @passed + @failed + @errors
190
+
191
+ puts "=" * 60
192
+ puts "Test Results Summary"
193
+ puts "=" * 60
194
+ puts "Total tests: #{total}"
195
+ puts "Passed: #{@passed} ✓"
196
+ puts "Failed: #{@failed} ✗" if @failed > 0
197
+ puts "Errors: #{@errors} ✗" if @errors > 0
198
+ puts
199
+
200
+ if @failed == 0 && @errors == 0
201
+ puts "SUCCESS: All tests passed!"
202
+ exit 0
203
+ else
204
+ puts "FAILURE: #{@failed + @errors} test(s) failed"
205
+ exit 1
206
+ end
207
+ end
208
+ end
209
+
210
+ # Run tests
211
+ if __FILE__ == $0
212
+ runner = XRPNTestRunner.new
213
+
214
+ if ARGV[0]
215
+ runner.run_test_file(ARGV[0])
216
+ runner.print_summary
217
+ else
218
+ runner.run_all_tests
219
+ end
220
+ end
@@ -0,0 +1,148 @@
1
+ # XRPN Test Program for Bug Fixes and Performance
2
+ # Tests critical bugs and performance issues
3
+
4
+ LBL "TESTALL"
5
+ "=== XRPN Bug Test ==="
6
+ aview
7
+ PSE
8
+
9
+ # Test 1: Division by zero
10
+ LBL "TEST1"
11
+ "Test 1: Division/0"
12
+ aview
13
+ PSE
14
+ 5
15
+ 0
16
+ /
17
+ "DIV/0 Failed"
18
+ aview
19
+ stop
20
+ LBL 01
21
+ "DIV/0 Caught OK"
22
+ aview
23
+ PSE
24
+
25
+ # Test 2: Statistics with empty registers
26
+ LBL "TEST2"
27
+ "Test 2: Stats/empty"
28
+ aview
29
+ PSE
30
+ clrg
31
+ mean
32
+ "Stats Failed"
33
+ aview
34
+ stop
35
+ LBL 02
36
+ "Stats Caught OK"
37
+ aview
38
+ PSE
39
+
40
+ # Test 3: File operations with non-existent file
41
+ LBL "TEST3"
42
+ "Test 3: Bad file"
43
+ aview
44
+ PSE
45
+ "nonexistent_file_12345.txt"
46
+ asto 01
47
+ rcl 01
48
+ getfile
49
+ "File err Failed"
50
+ aview
51
+ stop
52
+ LBL 03
53
+ "File err OK"
54
+ aview
55
+ PSE
56
+
57
+ # Test 4: Indirect addressing with nil register
58
+ LBL "TEST4"
59
+ "Test 4: IND nil"
60
+ aview
61
+ PSE
62
+ clrg
63
+ 100
64
+ sto 50
65
+ # Register 51 is nil/empty
66
+ 51
67
+ sto ind x
68
+ "IND nil Failed"
69
+ aview
70
+ stop
71
+ LBL 04
72
+ "IND nil OK"
73
+ aview
74
+ PSE
75
+
76
+ # Test 5: Numeric rounding overflow
77
+ LBL "TEST5"
78
+ "Test 5: Round ovfl"
79
+ aview
80
+ PSE
81
+ 9.9999999999
82
+ fix 2
83
+ # This should round to 10.00
84
+ "Rounding: "
85
+ arcl x
86
+ aview
87
+ PSE
88
+ 10
89
+ x!=y?
90
+ gto 05
91
+ "Round OK"
92
+ aview
93
+ PSE
94
+ gto "TEST6"
95
+ LBL 05
96
+ "Round Failed"
97
+ aview
98
+ stop
99
+
100
+ # Test 6: Statistics with data
101
+ LBL "TEST6"
102
+ "Test 6: Stats calc"
103
+ aview
104
+ PSE
105
+ clrg
106
+ 1
107
+ enter
108
+ 2
109
+ Σ+
110
+ 3
111
+ enter
112
+ 4
113
+ Σ+
114
+ 5
115
+ enter
116
+ 6
117
+ Σ+
118
+ mean
119
+ "Mean Y="
120
+ arcl y
121
+ " X="
122
+ arcl x
123
+ aview
124
+ PSE
125
+ # Should be Y=2, X=4
126
+ 2
127
+ x!=y?
128
+ gto 06
129
+ rdwn
130
+ 4
131
+ x!=y?
132
+ gto 06
133
+ "Stats calc OK"
134
+ aview
135
+ PSE
136
+ gto "DONE"
137
+ LBL 06
138
+ "Stats calc Failed"
139
+ aview
140
+ stop
141
+
142
+ LBL "DONE"
143
+ "=== All Tests Done ==="
144
+ aview
145
+ PSE
146
+ "Check for errors"
147
+ aview
148
+ end
@@ -0,0 +1,121 @@
1
+ LBL "TESTALL"
2
+ "XRPN Bug Test"
3
+ aview
4
+ PSE
5
+ xeq "TEST1"
6
+ xeq "TEST2"
7
+ xeq "TEST3"
8
+ xeq "TEST4"
9
+ xeq "TEST5"
10
+ xeq "TEST6"
11
+ "All Tests Done"
12
+ aview
13
+ end
14
+
15
+ LBL "TEST1"
16
+ "Test 1: Division/0"
17
+ aview
18
+ PSE
19
+ 5
20
+ 0
21
+ /
22
+ "DIV/0 Failed"
23
+ aview
24
+ stop
25
+
26
+ LBL "TEST2"
27
+ "Test 2: Stats/empty"
28
+ aview
29
+ PSE
30
+ clrg
31
+ mean
32
+ "Stats Failed"
33
+ aview
34
+ stop
35
+
36
+ LBL "TEST3"
37
+ "Test 3: Bad file"
38
+ aview
39
+ PSE
40
+ "nonexistent_file_12345.txt"
41
+ asto 01
42
+ rcl 01
43
+ getfile
44
+ "File err Failed"
45
+ aview
46
+ stop
47
+
48
+ LBL "TEST4"
49
+ "Test 4: IND nil"
50
+ aview
51
+ PSE
52
+ clrg
53
+ 100
54
+ sto 50
55
+ 51
56
+ sto ind x
57
+ "IND nil Failed"
58
+ aview
59
+ stop
60
+
61
+ LBL "TEST5"
62
+ "Test 5: Round ovfl"
63
+ aview
64
+ PSE
65
+ 9.9999999999
66
+ fix 2
67
+ "Rounding: "
68
+ arcl x
69
+ aview
70
+ PSE
71
+ 10
72
+ x!=y?
73
+ gto 05
74
+ "Round OK"
75
+ aview
76
+ PSE
77
+ rtn
78
+ LBL 05
79
+ "Round Failed"
80
+ aview
81
+ stop
82
+
83
+ LBL "TEST6"
84
+ "Test 6: Stats calc"
85
+ aview
86
+ PSE
87
+ clrg
88
+ 1
89
+ enter
90
+ 2
91
+ Σ+
92
+ 3
93
+ enter
94
+ 4
95
+ Σ+
96
+ 5
97
+ enter
98
+ 6
99
+ Σ+
100
+ mean
101
+ "Mean Y="
102
+ arcl y
103
+ " X="
104
+ arcl x
105
+ aview
106
+ PSE
107
+ 2
108
+ x!=y?
109
+ gto 06
110
+ rdwn
111
+ 4
112
+ x!=y?
113
+ gto 06
114
+ "Stats calc OK"
115
+ aview
116
+ PSE
117
+ rtn
118
+ LBL 06
119
+ "Stats calc Failed"
120
+ aview
121
+ stop
@@ -0,0 +1,4 @@
1
+ LBL "MAIN"
2
+ "Bug fixes test"
3
+ aview
4
+ end
@@ -0,0 +1,35 @@
1
+ LBL "MAIN"
2
+ "Starting tests"
3
+ aview
4
+ PSE
5
+
6
+ "Test div by zero"
7
+ aview
8
+ 5
9
+ 0
10
+ /
11
+ "DIV OK"
12
+ aview
13
+ PSE
14
+
15
+ "Test stats empty"
16
+ aview
17
+ clrg
18
+ mean
19
+ "STATS OK"
20
+ aview
21
+ PSE
22
+
23
+ "Test bad file"
24
+ aview
25
+ "nonexistent.txt"
26
+ asto 01
27
+ rcl 01
28
+ getfile
29
+ "FILE OK"
30
+ aview
31
+ PSE
32
+
33
+ "All tests done"
34
+ aview
35
+ end
data/xcmd/divide CHANGED
@@ -2,8 +2,12 @@ class XRPN
2
2
  # Return 1\/X to X
3
3
  def /
4
4
  @l = @x
5
- @x = @y / @x
6
- dropy
5
+ if @x == 0
6
+ throw :stop, "ERROR: Division by zero"
7
+ else
8
+ @x = @y / @x
9
+ dropy
10
+ end
7
11
  end
8
12
  end
9
13
 
data/xcmd/getfile CHANGED
@@ -5,7 +5,7 @@ class XRPN
5
5
  begin
6
6
  @x = File.read(@a).chomp
7
7
  rescue
8
- return "Cannot read the file \"#{a}\""
8
+ return "Cannot read the file \"#{@a}\""
9
9
  end
10
10
  end
11
11
  end
data/xcmd/mean CHANGED
@@ -4,8 +4,18 @@ class XRPN
4
4
  @l = @x
5
5
  lift
6
6
  lift
7
- @x = @reg[@srg] / @reg[@srg + 5]
8
- @y = @reg[@srg + 2] / @reg[@srg + 5]
7
+ # Check for nil registers and zero divisor
8
+ sum_x = @reg[@srg] || 0
9
+ sum_y = @reg[@srg + 2] || 0
10
+ count = @reg[@srg + 5] || 0
11
+
12
+ if count == 0
13
+ @x = 0
14
+ @y = 0
15
+ else
16
+ @x = sum_x / count
17
+ @y = sum_y / count
18
+ end
9
19
  end
10
20
  end
11
21
 
data/xcmd/rawinfo ADDED
@@ -0,0 +1,22 @@
1
+ class XRPN
2
+ # Display information about HP-41 RAW file
3
+ # Usage: "filename.raw" then RAWINFO
4
+ def rawinfo
5
+ filename = @a
6
+ if filename.empty?
7
+ puts "Error: No filename in Alpha register"
8
+ puts "Usage: Put RAW filename in Alpha, then run RAWINFO"
9
+ return
10
+ end
11
+
12
+ # Check file exists
13
+ unless File.exist?(filename)
14
+ puts "Error: File not found: #{filename}"
15
+ return
16
+ end
17
+
18
+ raw_info(filename)
19
+ end
20
+ end
21
+
22
+ # vim:ft=ruby:
data/xlib/_xrpn_version CHANGED
@@ -1,5 +1,5 @@
1
1
  def xrpn_version
2
- puts "XRPN version: 2.3"
2
+ puts "XRPN version: 2.6"
3
3
  puts "RubyGem version: " + Gem.latest_spec_for("xrpn").version.to_s
4
4
  end
5
5
 
data/xlib/debug_mode CHANGED
@@ -1,5 +1,6 @@
1
1
  def debug_mode
2
- #prompt = TTY::Prompt.new
2
+ # Lazy initialization of TTY::Prompt
3
+ @debugprompt ||= TTY::Prompt.new
3
4
  loop do
4
5
  if $prompt
5
6
  @line = @debugprompt.ask(":")
data/xlib/ind CHANGED
@@ -11,7 +11,8 @@ def ind(l3)
11
11
  when "l"
12
12
  l2 = @p.l.to_i.to_s
13
13
  else
14
- l2 = @p.reg[l3].to_i.to_s
14
+ reg_val = @p.reg[l3] || 0
15
+ l2 = reg_val.to_i.to_s
15
16
  end
16
17
  l2 = "%02d" % [l2] if l2 == l2.to_i.to_s
17
18
  return l2
data/xlib/numeric CHANGED
@@ -36,12 +36,13 @@ class Numeric
36
36
  s = x.to_i.to_s
37
37
  self < 0 ? m = "-" : m = ""
38
38
  s.sub!(/-/, '')
39
- f = (x.frc * 10 ** i).round.to_i.to_s
40
- if f.length > i # Fix rounding overflow
41
- s = (x + 1).to_i.to_s
42
- f = f[1..-1]
39
+ f_val = (x.frc * 10 ** i).round
40
+ if f_val >= 10 ** i # Fix rounding overflow
41
+ s = (x.to_i + 1).to_s
42
+ f = "0" * i
43
+ else
44
+ f = f_val.to_s.rjust(i, '0')
43
45
  end
44
- f = f.to_s
45
46
  i > 0 ? s.sub!(/(\d*\.)(\d{,#{i}}).*/, '\1\2') : s.sub!(/(\d*).*/, '\1')
46
47
  if e.abs >= n # If exponent kicks in
47
48
  s += "." + f.ljust(i, "0")
data/xlib/raw_info ADDED
@@ -0,0 +1,155 @@
1
+ # HP-41 RAW File Information Viewer
2
+ # Displays labels, strings, and basic info from HP-41 RAW files
3
+
4
+ def raw_info(filename)
5
+ begin
6
+ data = File.binread(filename)
7
+
8
+ puts "=" * 60
9
+ puts "HP-41 RAW File: #{File.basename(filename)}"
10
+ puts "File size: #{data.length} bytes"
11
+ puts "=" * 60
12
+ puts
13
+
14
+ # Extract labels
15
+ labels = []
16
+ strings = []
17
+ pos = 0
18
+
19
+ while pos < data.length
20
+ byte = data[pos].ord
21
+
22
+ # Global label (C0 00 Fx yy LABELNAME)
23
+ if byte == 0xC0 && pos + 3 < data.length && data[pos + 1].ord == 0x00
24
+ label_type = data[pos + 2].ord
25
+ pos += 4
26
+
27
+ # Extract label name
28
+ label = ""
29
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
30
+ label << data[pos]
31
+ pos += 1
32
+ end
33
+
34
+ type_str = case label_type
35
+ when 0xF4 then "Alpha"
36
+ when 0xF5 then "Numeric"
37
+ else "Unknown"
38
+ end
39
+
40
+ labels << " LBL \"#{label}\" (#{type_str})"
41
+ next
42
+ end
43
+
44
+ # Local label (F6 00 LABELNAME)
45
+ if byte == 0xF6 && pos + 1 < data.length && data[pos + 1].ord == 0x00
46
+ pos += 2
47
+
48
+ # Extract label name
49
+ label = ""
50
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
51
+ label << data[pos]
52
+ pos += 1
53
+ end
54
+
55
+ labels << " LBL \"#{label}\" (Local)"
56
+ next
57
+ end
58
+
59
+ # Text strings (multiple formats)
60
+ # Format 1: 7F 20 TEXT
61
+ if byte == 0x7F && pos + 1 < data.length && data[pos + 1].ord == 0x20
62
+ pos += 2
63
+ text = ""
64
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
65
+ text << data[pos]
66
+ pos += 1
67
+ end
68
+ strings << " \"#{text}\"" unless text.empty?
69
+ next
70
+ end
71
+
72
+ # Format 2: FD TEXT (string literals in prompts)
73
+ if byte == 0xFD && pos + 1 < data.length
74
+ pos += 1
75
+ text = ""
76
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
77
+ text << data[pos]
78
+ pos += 1
79
+ end
80
+ strings << " \"#{text}\"" unless text.empty?
81
+ next
82
+ end
83
+
84
+ # Format 3: F8 TEXT (another string format)
85
+ if byte == 0xF8 && pos + 1 < data.length
86
+ pos += 1
87
+ text = ""
88
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
89
+ text << data[pos]
90
+ pos += 1
91
+ end
92
+ strings << " \"#{text}\"" unless text.empty?
93
+ next
94
+ end
95
+
96
+ # Format 4: F3 TEXT (yet another string format)
97
+ if byte == 0xF3 && pos + 1 < data.length
98
+ pos += 1
99
+ text = ""
100
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
101
+ text << data[pos]
102
+ pos += 1
103
+ end
104
+ strings << " \"#{text}\"" unless text.empty?
105
+ next
106
+ end
107
+
108
+ # Format 5: FB TEXT (text string marker)
109
+ if byte == 0xFB && pos + 1 < data.length
110
+ pos += 1
111
+ text = ""
112
+ while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
113
+ text << data[pos]
114
+ pos += 1
115
+ end
116
+ strings << " \"#{text}\"" unless text.empty?
117
+ next
118
+ end
119
+
120
+ pos += 1
121
+ end
122
+
123
+ # Display findings
124
+ puts "Labels found (#{labels.length}):"
125
+ if labels.empty?
126
+ puts " (none)"
127
+ else
128
+ labels.each { |l| puts l }
129
+ end
130
+ puts
131
+
132
+ puts "Text strings found (#{strings.length}):"
133
+ if strings.empty?
134
+ puts " (none)"
135
+ else
136
+ strings.each { |s| puts s }
137
+ end
138
+ puts
139
+
140
+ puts "Hex dump (first 256 bytes):"
141
+ puts "-" * 60
142
+ system("hexdump -C '#{filename}' | head -20")
143
+ puts
144
+
145
+ puts "Note: Full RAW to XRPN conversion requires comprehensive"
146
+ puts "HP-41 bytecode documentation. This viewer extracts readable"
147
+ puts "labels and strings for informational purposes."
148
+ puts "=" * 60
149
+
150
+ rescue => e
151
+ puts "Error reading RAW file: #{e.message}"
152
+ end
153
+ end
154
+
155
+ # vim:ft=ruby:
data/xrpn.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'xrpn'
3
- s.version = '2.4'
3
+ s.version = '2.6'
4
4
  s.licenses = ['Unlicense']
5
5
  s.summary = "XRPN - The eXtended RPN (Reverse Polish Notation) programming language"
6
- s.description = "A full programming language and environment extending the features of the venerable HP calculator programmable calculators. With XRPN you can accomplish a wide range of modern programming tasks as well as running existing HP-41 FOCAL programs directly. XRPN gives modern life to tens of thousands of old HP calculator programs and opens the possibilities to many, many more. New in 2.4: Fixed loading of files via the -f switch."
6
+ s.description = "A full programming language and environment extending the features of the venerable HP calculator programmable calculators. With XRPN you can accomplish a wide range of modern programming tasks as well as running existing HP-41 FOCAL programs directly. XRPN gives modern life to tens of thousands of old HP calculator programs and opens the possibilities to many, many more. New in 2.6: Comprehensive regression test framework with 58 automated tests and HP-41 RAW file viewer for inspecting thousands of legacy HP-41 programs."
7
7
 
8
8
  s.authors = ["Geir Isene"]
9
9
  s.email = 'g@isene.com'
@@ -11,6 +11,6 @@ Gem::Specification.new do |s|
11
11
 
12
12
  s.add_runtime_dependency 'tty-prompt', '~> 0.23'
13
13
 
14
- s.files = ["bin/xrpn", "conf", "theme_example", "README.md", "xrpn.gemspec"] + Dir.glob("xcmd/*") + Dir.glob("xlib/*")
14
+ s.files = ["bin/xrpn", "conf", "theme_example", "README.md", "xrpn.gemspec"] + Dir.glob("xcmd/*") + Dir.glob("xlib/*") + Dir.glob("tests/*")
15
15
  s.executables << 'xrpn'
16
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xrpn
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.4'
4
+ version: '2.6'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-30 00:00:00.000000000 Z
11
+ date: 2025-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-prompt
@@ -28,8 +28,9 @@ description: 'A full programming language and environment extending the features
28
28
  the venerable HP calculator programmable calculators. With XRPN you can accomplish
29
29
  a wide range of modern programming tasks as well as running existing HP-41 FOCAL
30
30
  programs directly. XRPN gives modern life to tens of thousands of old HP calculator
31
- programs and opens the possibilities to many, many more. New in 2.4: Fixed loading
32
- of files via the -f switch.'
31
+ programs and opens the possibilities to many, many more. New in 2.6: Comprehensive
32
+ regression test framework with 58 automated tests and HP-41 RAW file viewer for
33
+ inspecting thousands of legacy HP-41 programs.'
33
34
  email: g@isene.com
34
35
  executables:
35
36
  - xrpn
@@ -39,6 +40,13 @@ files:
39
40
  - README.md
40
41
  - bin/xrpn
41
42
  - conf
43
+ - tests/README.md
44
+ - tests/final_test.xrpn
45
+ - tests/run_tests.rb
46
+ - tests/test_bugs.xrpn
47
+ - tests/test_bugs_simple.xrpn
48
+ - tests/test_final.xrpn
49
+ - tests/test_simple.xrpn
42
50
  - theme_example
43
51
  - xcmd/abs
44
52
  - xcmd/acos
@@ -220,6 +228,7 @@ files:
220
228
  - xcmd/r_p
221
229
  - xcmd/rad
222
230
  - xcmd/rand
231
+ - xcmd/rawinfo
223
232
  - xcmd/rcl
224
233
  - xcmd/rclaf
225
234
  - xcmd/rclflag
@@ -328,6 +337,7 @@ files:
328
337
  - xlib/locate_prg
329
338
  - xlib/numeric
330
339
  - xlib/numformat
340
+ - xlib/raw_info
331
341
  - xlib/read_state
332
342
  - xlib/read_xcmd
333
343
  - xlib/rxcmd