Babbage's Difference Engine in Brainfuck on a Calculator

in programming •  last year  (edited)

tl;dr: The simplicity of this 1982 programmable calculator makes it possible to become a "guru" just by reading the manual: Since there are no abstraction layers that can fail you can just write whatever you want without wasting your time with demotivating nonsense like googling error messages or seeking through stackoverflow answers. Just code and it works!

Brainfuck Difference Engine on HP 15C Calculator Hardware Clone

I was just sitting in a park, listening to some podcast and wondered "Hey, why not write a Brainfuck interpreter for my calculator?". So I grabbed my notebook and started scribbling.

Notebook

Brainfuck?

It's a programming language that only has eight instructions. The instructions operate on a tape of numbers.

InstructionMeaning
>Move tape head to the right
<Move tape head to the left
+Increment value at tape head position
-Decrement value at tape head position
.Print value at tape head position
,Read value from input to tape head position
[Loop: Jump to after the corresponding ] if the value at the tape head position is 0
]Loop: Jump to after the corresponding [ if the value at the tape head position is not 0

For example, this program prompts the user for two numbers and then adds them together:

,>,[-<+>]<.

For more details, you can read the Wikipedia article on Brainfuck.

HP 15C

It's a programmable calculator from 1982 that has 20 registers of storage and the ability to do indirect register addressing and indirect jumping to labels based on register content, which comes in really handy when interpreting numbers as instructions. See My previous blog post "Programming like it's 1981" about this thing for more details.

Implementing the Interpreter

An interpreter is a program that looks at data and interprets it as a program i.e. executing the instructions encoded in the data. In the case of the 15C calculator this means that there is an interpreter program written in the calculator's unnamed programming language that reads the numbers that are stored in the registers and behaves as if those numbers were Brainfuck instructions. So the first thing we need is a mapping from instructions to numbers:

InstructionNumber
>1
<2
+3
-4
.5
,6
[7
]8

For example, the program "+[.+]", which prints all natural numbers, would be [3, 7, 5, 3, 8].

Instruction Pointer

If we put the instructions into the registers we need to keep track of which instruction to execute next: One register is used as the instruction pointer and contains the number of the register holding the next instruction to be executed; So we fetch the number from that register, execute the instruction, then add 1 to the instruction pointer and repeat.

Tape Pointer

Since Brainfuck uses a tape head that moves across a tape of numbers, we need to store the tape head position in another register. The ">" command moves the tape head to the right, "<" to the left.

However, since we only have 20 registers in total, we should place code and tape on opposite ends of the register space, which means that the first tape cell is the last register, the second tape cell is the second to last register etc. and the ">" command decreases the tape pointer register by 1 to move "right".

Loops

Brainfuck loops can be nested to an arbitrary depth. A fast implementation would probably use some sort of look-up table for matching the loop brackets, but this would take too many registers so instead I opted for just one register that contains a "loop nesting number".

Consider this program:

[+++++[++++++]++++++]++

The first "[" skips the block if the cell at the tape head position is zero. One solution for finding the matching "]" is to set the loop nesting number to 1, then read the program in the right direction and count each "[" as +1 and each "]" as -1. So when encountering the second "[" in the program, the loop nesting number becomes 2, on the next "]" it becomes 1 again and on the final "]" it becomes 0, which means we found the end of the current block and the program can now resume normal execution.

Jumping from the end of a block to the start works similarly: set the loop nesting number to -1 and seek backwards.

Memory Layout

So we need three special purpose registers, followed by Brainfuck code, followed by the tape. We can allocate the registers like this:

RegisterContent
0Instruction pointer
1Tape pointer
2Loop nesting number
3Begin of program code
...More program code
...0 (Signals end of program code)
...More tape
19Begin of tape

Note that the tape grows backwards from the end of register space. Note that programs can corrupt themselves since there is no bounds checking.

Compacting code

Since 20 registers are not enough to write any interesting programs at one instruction per register, we need a way to put more than one instruction in each register.

The HP 15C uses decimal numbers with a 10 digit mantissa, a 2 digit exponent and a sign, which means we can fit one instruction per digit. Consider this program that calculates fibonacci numbers:

+>+[[->+>+<<]<[->>>+<<<]>>>.]

At one digit per instruction, this fits into just 3 registers:

3137741313
2282741113
2228111580

(Notice that the last number is padded with a 0 on the right side since the code is stored as big endian)

If we want to keep both the fast "one instruction per register"- and the compact "ten instructions per register"-mode we could use flag 0 to distinguish which mode to use.

Compiling code

The mapping of one instruction per register or digit is simple enough to write code directly on the calculator. However, it would be more convenient if we had a compiler that turns Brainfuck code into numbers.

Here's a ruby script that does exactly that:

instruction_table = {
  ">" => 1,
  "<" => 2,
  "+" => 3,
  "-" => 4,
  "." => 5,
  "," => 6,
  "[" => 7,
  "]" => 8
}

program = ARGV[0]
instructions = program.chars
codes = instructions.map { |i| instruction_table[i] }

# Fast program (for programs up to 15 commands long)
if(codes.size <= 15) then
  registers = codes.map.with_index { |instruction, index|  "#{instruction} STO #{index + 3}" }

  puts 
  puts "Fast"
  puts "===="
  puts "CF 0"
  puts registers
end

# Compact program
chunks = codes.each_slice(10).to_a.map { |a| a.fill(0, a.size...10) }
registers = chunks.map.with_index { |line, index| "#{line.join} STO #{index + 3}"}

puts
puts "Compact"
puts "======="
puts "SF 0"
puts registers
puts

Just save this as bf.rb and start it like ruby bf.rb "+[.+]" to get an output like this:

Fast
====
CF 0
3 STO 3
7 STO 4
5 STO 5
3 STO 6
8 STO 7

Compact
=======
SF 0
3753800000 STO 3

For programs that won't fit in Fast mode, only the Compact version will be shown:

ruby ../utils/bf_compile.rb "+>+[[->+>+<<]<[->>>+<<<]>>>.]"

Compact
=======
SF 0
3137741313 STO 3
2282741113 STO 4
2228111580 STO 5

Charles Babbage's Difference Engine

The Difference Engine was a machine to tabulate polynomials using divided differences. It wasn't programmable, but it ran a program that was parameterizable so you could use it to calculate different functions, e.g. logarithmic tables or sine tables.

Difference Engine

Here is a simple implementation in Brainfuck: ,>>,>>,>+[<[-<+<+>>]<[->+<]<[-<+<+>>]<<.>[->+<]>>>>]

You can enter it into the calculator like this:

SF 0
6116116137 STO 3
2742323118 STO 4
2741328274 STO 5
2323118225 STO 6
1741328111 STO 7
1800000000 STO 8

When you run this program, it will ask you for three numbers corresponding to the initial number column setting of the Difference Engine. Examples:

InputOutput
0, 1, 0Natural numbers (1, 2, 3 ...)
1, 1, 2Square numbers (1, 4, 9 ...)

So there you have it: A running Difference Engine, implemented in Brainfuck, running on a HP 15C calculator from the eighties :)

Program

Now all you have to do is enter the following code into your calculator. After you entered your program, run B to start the interpreter.

  • Output will be displayed for one second.
  • When input is required, the display will flash. Simply enter a number and hit R/S to continue.
  • When the program terminates it will try to display the last output.

Registers

RegisterContent
0Instruction Pointer
1Tape Pointer
2Loop nesting number
3Begin of program code
...More program code
...0 (Signals end of program code)
...More tape
19Begin of tape

Flags

Flag 0 is meant to be set/cleared by the user.

FlagStatusMeaning
0clearFast mode (1 instruction per register)
0setCompact mode (10 instructions per register)
1clearSeek forward to matching bracket
1setSeek backward to matching bracket

Labels

LabelContent
BRun Brainfuck program
CClear tape
EvRCL digit
0End program
1-8Run corresponding Brainfuck instruction
9Seek to matching bracket
10Fetch current instruction
11Execution loop
12Clear tape loop

Code Listing

; init registers
001 - LBL B
  FIX 0
  GSB C
  10
  ENTER
  3
  F? 0
    *
  STO 0
  1
  9
  STO 1
  0
  STO 2

; run program
012 - LBL 11

  ; fetch instruction
  GSB 10

  ; execute instruction
  STO I
  GSB I

  ; increment instruction pointer
  1
  STO + 0

  ; execute next instruction
  GTO 11

; clear tape
019 - LBL C
  CF 1
  3.019
  STO I
  LBL 12
    RCL (i)
    x = 0 ?
      SF 1
    F? 1
      CLx
    STO (i)
    ISG I
      GTO 12
  RTN

; special "stop program" instruction
037 - LBL 0
  ; display last output for convenience
  R/\
  R/S

; ">" (Move tape head to the right)
044 - LBL 1
  1
  STO - 1
  RTN

; "<" (Move tape head to the left)
048 - LBL 2
  1
  STO + 1
  RTN

; "+" (Increment value at tape head position)
052 - LBL 3
  RCL 1
  STO I
  1
  STO + (i)
  RTN

; "-" (Decrement value at tape head position)
058 - LBL 4
  RCL 1
  STO I
  1
  STO - (i)
  RTN

; "." (Output value at tape head position)
064 - LBL 5
  RCL 1
  STO I
  RCL (i)
  PSE
  RTN

; "," (Input value to tape head position)
070 - LBL 6
  RCL 1
  STO I
  CLx
  SF 9 ; blink
  R/S
  CF 9
  STO (i)
  RTN

; "[" (begin loop)
079 - LBL 7
  RCL 1
  STO I
  RCL (i)
  x != 0 ?
    RTN
  0
  STO 2
  CF 1 ; seek forward
  GTO 9

; "]" (end loop)
089 - LBL 8
  RCL 1
  STO I
  RCL (i)
  x = 0 ?
    RTN
  0
  STO 2
  SF 1

098 - LBL 9 ; seek to matching bracket
    GSB 10
    6
    -
    1
    x = y ?
      STO + 2
    GSB 10
    7
    -
    1  ; needed because GSB 10 modifies the stack
    x = y ?
      STO - 2
    RCL 2
    x = 0 ?
      RTN
    1
    F? 1
      CHS
    STO + 0
    GTO 9

; fetch current instruction
119 - LBL 10
  RCL 0
  F? 0
    GTO E
  STO I
  RCL (i)
  RTN

; vRCL digit
126 - LBL E
  ; find correct register
  1
  0
  /
  STO I

  ; find digit index (reversed)
  FRAC
  1
  0
  *
  LSTx
  x><y
  -
  10^x
  RCL (i)
  x><y
  /
  FRAC
  1
  0
  *
  INT
147 - RTN

Example Programs

Add two numbers

,>,[-<+>]<.

 Fast
====
CF 0
6 STO 3
1 STO 4
6 STO 5
7 STO 6
4 STO 7
2 STO 8
3 STO 9
1 STO 10
8 STO 11
2 STO 12
5 STO 13

Compact
=======
SF 0
6167423182 STO 3
5000000000 STO 4

Multiply two numbers

,>,<[>[->+>+<<]>[-<+>]<<-]>>>.

SF 0
6162717413 STO 3
1322817423 STO 4
1822481115 STO 5

Fibonacci Numbers

+>+[[->+>+<<]<[->>>+<<<]>>>.]

SF 0
3137741313 STO 3
2282741113 STO 4
2228111580 STO 5

Difference Engine

,>>,>>,>+[<[-<+<+>>]<[->+<]<<[-<+<+>>]<<.>[->+<]>>>>]

SF 0
6116116137 STO 3
2742323118 STO 4
2741328227 STO 5
4232311822 STO 6
5174132811 STO 7
1180000000 STO 8

Learnings

It's insane how simple and productive it is to write something for the calculator: There is absolutely zero nonsense involved, all you need to know is in the manual, you will never have to look something up on stack overflow, there are no nonsensical errors, you won't have to clear the cache of the IDE etc. Surely there must be a middleground between working on a high tower of faulty abstraction layers and writing in this oldschool machine language.

Software development should be a lot simpler, it's insane how much time is spent on irrelevant nonsense.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Congratulations @michaelzinn! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

Are you a DrugWars early adopter? Benvenuto in famiglia!
Vote for @Steemitboard as a witness to get one more award and increased upvotes!