Login [Register]
Don't have an account? Register now to chat, post, use our tools, and much more.
Rebec is a programming language I've been working on that compiles to a single-instruction virtual machine.

This has been a long-term project I started awhile ago and have been working on it on and off for awhile now.

The great thing about the virtual machine is that it is so simple, it is very easy to port to any platform. So I ported it to the TI-84+. I plan to make it support the TI-84+CSE and TI-84+CE as well. This means all code written in Rebec can run on the TI-84+.

I plan to eventually build this virtual machine into a physical CPU. What the virtual machine does is simulate the imaginary CPU, and any time a write is made to the output pins of the virtual CPU, it displays the lower 8 bits as an ASCII character on the screen, and every time a read is made from the input pins, it pauses and lets the user type in what they want the values of the input pins to be at that time (in ASCII characters). So with the virtual machine you can use that to read and write characters to the screen, which can be used for interactive programs.

All of this will be on this Github. The project R313 is the assembler and virtual machine for PC. The project Z313 is the virtual machine for the TI-83+/TI-84+ calculators. The project Rebec is the compiler for the Rebec programming language. The project PI313 is a virtual machine for the Raspberry Pi that connects the virtual CPU's output and input pins to the physical CPU of the Raspberry Pi, allowing you to control external hardware with Rebec code.

To give you an example of this programming language, here's a simple "Hello, World!" program:




Code:
;Define the data we will be using.
def string, "Hello, World!", 10, 0
fct main:
   ldi print_in, string
   cal print
   end

;Input: print_in (pointer)
;Output: Prints a null-terminated string to the screen.
def print_in, 0
def print_char, 0
def print_zero, 0
fct print:
   ;Load into print_char the word print_in is pointing to.
   lbr print_char, print_in

   ;Set the output pins' value to what's stored in char.
   ;  This will be written to the screen as an ASCII character
   ;  in the virtual machine.
   set print_char

   ;Increment the pointer.
   inc print_in

   ;Load the next character of our string by reference.
   lbr print_char, print_in

   ;Jump to main if char is not zero.
   jne print, print_char, print_zero
   
   ;Return
   ret


To get on the calculator, you first need to compile it, then assemble it, then convert it to an AppVar, then transfer it to the calculator. These are the commands to do so:


Code:
rebec hello.rbc > hello.asm
rasm hello.asm hello.rbx
gen8xv hello.rbx


"rebec" is the programming language's compiler, "rasm" is the assembler for R313 assembly (the name I gave to the virtual machine's architecture), and "gen8xv" generates an appvar from a binary file.

You transfer this to your calc then you will have a program on your calc called "HELLO" and you can run it with:


Code:
"HELLO":Asm(Z131


The basics of Rebec

Rebec is the programming language that makes this a hell of a lot easier for the layman to program this virtual machine. The language is a wee bit ugly, I've never made a real programming language before and it's all coded in C. But it works. It compiles down to R313 assembly.

Semicolons are for comments. Rebec has "commands", and each command is 3 letters.

There are three different types of commands:

1. Definitions (def).
2. Functions/labels (fct/lbl).
3. Logic (anything else...).

Definitions are sort of like your variables. All variables are global. It's highly recommended to define all the data you need at the top of any function you create. Don't be afraid to use a lot of definitions if you have to. There's no speed penalty for defining a lot of things. (You are limited to 65k words of RAM though.)

Functions/labels can either be called or jumped to. "fct" and "lbl" both are identical keywords standing for "function" and "label". They literally do the exact same things and are interchangeable. But for neatness, I recommend using these differently.

You call functions but you jump to labels. Use labels while within a function to make it easier to understand where your function begins and where it ends.

Example:


Code:
fct myFunction:
   ;...some code...
lbl myLabel1:
   ;...some code...
lbl myLabel2:
   ;...some code...
   ret



Rebec implements a stack in R313 assembly for you, so you can call and return from subroutines. I wouldn't recommend using recursion, though, because the stack is rather small.

The final type of command, the logic commands, are everything else.
These take a number of arguments and do something.

Here's some examples:


Code:
;"Load immediate": Loads an immediate value into x.
   ldi x, 25
;"Load": Loads the value stored in y into x.
   lod x, y
;"Load by reference": Loads what y is pointing to into x.
   lbr x, y
;"Store": Stores the value of x into y.
   sto x, y
;"Store by reference": Stores the value of x into the what y is pointing to.
   sbr x, y
;"Jump": Jumps to a label or a function named x.
   jmp x
;"Jump if greater than or equal to": Jumps to x if y >= z.
   jge x, y, z
;"Jump if less than": Jumps to x if y < z.
   jpl x, y, z
;"Jump if equal to": Jumps to x if y = z.
   jpe x, y, z
;"Jump if not equal to": Jumps to x if y != z.
   jne x, y, z
;"Jump if not negative": Jumps to x if y is not negative.
   jnn x, y
;"Call": Calls a subroutine named x.
   cal x
;"Return": Returns from a subroutine.
   ret
;"End": Puts the CPU in the halting state.
   end
;"Set the output pins": Writes the value of x to the output pins.
   set x
;"Get the input pins": Read the value of the input pins into x.
   get x
;"Addition": Adds x and y together and stores the result into x.
   add x, y
;"Subtraction": Subtracts y from x and stores the result into x.
   sub x, y
;"Increment": Adds 1 to x.
   inc x
;"Decrement": Subtracts 1 from x.
   dec x
;"Negation": Negates the value of x.
   neg x
;"Clear": Sets the value of x to 0.
   clr x


Understanding these main commands you can see how the "Hello, World!" program works.

Notice that the only mathematical operation Rebec has is addition and subtraction. There are no bitwise operations nor are there multiplication, division, modulo, etc. Why? Because the virtual machine only has one instruction, thus one math operation (add/subtract). Anything like multiplation or bitwise operations would require a lot of code to write, so I leave that up to the programmer to import a library or something (currently I don't have an "#include" command but I will add one eventually). I am working on a library for bitwise stuff which you could then create multiplication/division/modulo out of.

The "Hello, World" code compiles down to R313, here is the assembly code that the program generates:


Code:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;               Start of Code Segment                 ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
main:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadImmidiate(A, B) {                               ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   rssb print_in
   rssb print_in
   rssb print_in
   rssb print_in
   rssb $+7
   rssb zero
   rssb zero
   rssb print_in
   rssb acc
   rssb acc
   rssb neg
   rssb string
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Call() {                                            ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Store our location in the stack.
   rssb $+34
   rssb $+33
   rssb $+32
   rssb $+31
   rssb ___REBEC_STACK_POINTER___
   rssb zero
   rssb zero
   rssb $+27
   rssb $+27
   rssb $+26
   rssb $+25
   rssb $+24
   rssb ___REBEC_STACK_POINTER___
   rssb zero
   rssb zero
   rssb $+20
   rssb $+20
   rssb $+19
   rssb $+18
   rssb $+17
   rssb ___REBEC_STACK_POINTER___
   rssb zero
   rssb zero
   rssb $+13
   rssb $+18
   rssb $+17
   rssb $+16
   rssb $+15
   rssb ___REBEC_STACK_POINTER___
   rssb zero
   rssb zero
   rssb $+11
   rssb acc
   rssb acc
   rssb 0
   rssb 0
   rssb 0
   rssb acc
   rssb acc
   rssb $+7
   rssb zero
   rssb zero
   rssb 0
   rssb acc
   rssb acc
   rssb neg
   rssb $+17
;Increment the stack pointer.
   rssb acc
   rssb acc
   rssb pos
   rssb zero
   rssb zero
   rssb ___REBEC_STACK_POINTER___
;Jump to the call.
   rssb acc
   rssb acc
   rssb $+7
   rssb zero
   rssb zero
   rssb pc
   rssb acc
   rssb acc
   rssb neg
   rssb print-$+3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Exit() {                                            ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   rssb acc
   rssb acc
   rssb pc
   rssb pc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

print:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadByReference(A, B) {                             ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Load the location to store the data.
   rssb $+12
   rssb $+11
   rssb $+10
   rssb $+9
   rssb print_in
   rssb zero
   rssb zero
   rssb $+5
;Store the data.
   rssb print_char
   rssb print_char
   rssb print_char
   rssb print_char
   rssb 0
   rssb zero
   rssb zero
   rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Set(A) {                                            ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Store the set character.
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb print_char
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
;Negate the input.
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb print_char
   rssb print_char
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb print_char
;Add our previous output to the input.
   rssb acc
   rssb acc
   rssb ___REBEC_PREVIOUS_OUTPUT___
   rssb zero
   rssb zero
   rssb print_char
;Subtract the input from the previous output.
   rssb acc
   rssb acc
   rssb print_char
   rssb ___REBEC_PREVIOUS_OUTPUT___
   rssb acc
   rssb acc
   rssb print_char
   rssb neg
   rssb ___REBEC_PREVIOUS_OUTPUT___
;Subtract the input from the output
   rssb acc
   rssb acc
   rssb print_char
   rssb out
   rssb acc
   rssb acc
   rssb print_char
   rssb neg
   rssb out
;Negate the input.
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb print_char
   rssb print_char
   rssb ___REBEC_TEMP_1___
   rssb print_char
   rssb print_char
;Restore the set character.
   rssb print_char
   rssb print_char
   rssb print_char
   rssb print_char
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb zero
   rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Incrementation(A) {                                 ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   rssb acc
   rssb acc
   rssb pos
   rssb zero
   rssb zero
   rssb print_in
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; LoadByReference(A, B) {                             ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Load the location to store the data.
   rssb $+12
   rssb $+11
   rssb $+10
   rssb $+9
   rssb print_in
   rssb zero
   rssb zero
   rssb $+5
;Store the data.
   rssb print_char
   rssb print_char
   rssb print_char
   rssb print_char
   rssb 0
   rssb zero
   rssb zero
   rssb print_char
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; JumpIfNotEqual(A, B) {                           ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Clear the temporary register..
;Subtract the second value from the first.
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb print_char
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb print_zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb print_zero
   rssb neg
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb pos
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb neg
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
;If the input is negative then replace `rssb pc` within
;  this segment with the negation of the input. Since
;  the negation of the input is always -1 if it is
;  negative, then `rssb pc` will always be overwritten
;  with the value 1, which is the same as `rssb acc`.
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb $+58
;Repeat this for the other `rssb pc`.
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb $+58
;Subtract the first value from the second.
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb print_zero
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb print_char
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb print_char
   rssb neg
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb pos
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb neg
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
;If the input is negative then replace `rssb pc` within
;  this segment with the negation of the input. Since
;  the negation of the input is always -1 if it is
;  negative, then `rssb pc` will always be overwritten
;  with the value 1, which is the same as `rssb acc`.
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb $+9
;Repeat this for the other `rssb pc`.
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb $+9
;Jump to the given address if it is positive.
   rssb acc
   rssb acc
   rssb $+16
   rssb pc
;Jump to the given address if negative.
   rssb acc
   rssb acc
   rssb $+16
   rssb zero
   rssb pc
;Restore the two removed `rssb pc`.
   rssb acc
   rssb acc
   rssb $-3
   rssb $-4
   rssb $-10
   rssb $-11
;Storage for the address to jump to if positive.
   rssb acc
   rssb acc
   rssb neg
   rssb -30
;Storage for the address to jump to if negative.
   rssb acc
   rssb acc
   rssb neg
   rssb -25
;Jump to the given address.
   rssb acc
   rssb acc
   rssb $+7
   rssb zero
   rssb zero
   rssb pc
   rssb acc
   rssb acc
   rssb neg
   rssb print-$+3
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Return() {                                          ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Decrement the stack pointer
   rssb acc
   rssb acc
   rssb neg
   rssb zero
   rssb zero
   rssb ___REBEC_STACK_POINTER___
;Load the stack pointer
   rssb $+12
   rssb $+11
   rssb $+10
   rssb $+9
   rssb ___REBEC_STACK_POINTER___
   rssb zero
   rssb zero
   rssb $+5
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb ___REBEC_TEMP_2___
   rssb 0
   rssb zero
   rssb zero
   rssb ___REBEC_TEMP_2___
;Subtract from here
   rssb acc
   rssb acc
   rssb $+10
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb $+6
   rssb neg
   rssb ___REBEC_TEMP_2___
   rssb acc
   rssb acc
   rssb neg
   rssb $+7
;Jump to the address.
   rssb acc
   rssb acc
   rssb ___REBEC_TEMP_2___
   rssb zero
   rssb zero
   rssb pc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; }                                                   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;                End of Code Segment                  ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;               Start of Data Segment                 ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
___REBEC_TEMP_1___:
   rssb 0
___REBEC_TEMP_2___:
   rssb 0
___REBEC_TEMP_3___:
   rssb 0
___REBEC_TEMP_4___:
   rssb 0
___REBEC_PREVIOUS_OUTPUT___:
   rssb 0
___REBEC_STACK_POINTER___:
   rssb ___REBEC_STACK___
___REBEC_STACK___:
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
   rssb 0
string:
   rssb 'H'
   rssb 'e'
   rssb 'l'
   rssb 'l'
   rssb 'o'
   rssb ','
   rssb ' '
   rssb 'W'
   rssb 'o'
   rssb 'r'
   rssb 'l'
   rssb 'd'
   rssb '!'
   rssb 10
   rssb 0
print_in:
   rssb 0
print_char:
   rssb 0
print_zero:
   rssb 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;                End of Data Segment                  ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


As you can tell, coding the "Hello, World!" program in R313 assembly would be rather tedious. That's why I created Rebec.

Here's Rebec code that asks the user "what's your name?" and waits for input, then spits out "Hello, [name]!" It only accepts up to 3 letters for the name.


Code:
;Define the data we will be using.
def char, 0
def str_intro, "What is your name?: ", -1
def str_response, "Hello, ", -1
def str_name, 0, 0, 0, -1
def str_final, "!", 10, -1
def print_arg, 0
def read_arg, 0

;The main function.
fct main:

   ldi print_arg, str_intro
   cal print
   ldi read_arg, str_name
   cal read
   ldi print_arg, str_response
   cal print
   ldi print_arg, str_name
   cal print
   ldi print_arg, str_final
   cal print

   end

fct read:
   get char
   sbr char, read_arg
   inc read_arg
   lbr char, read_arg
   jnn read, char
   ret

fct print:
   lbr char, print_arg
   set char
   inc print_arg
   lbr char, print_arg
   jnn print, char
   ret


Notice here I use "jnn". That's because it's a little more efficient than "jpe" or "jne".

Here's it running on the calculator:



Here's some PI313 code I wrote in Rebec to draw a smiley face to an 8x8 LED matrix display (the display is connected to a Raspberry Pi):




Code:
def row0, "0000000000000000", 0
def row1, "0000000000100100", 0
def row2, "0000000000100100", 0
def row3, "0000000000100100", 0
def row4, "0000000010000001", 0
def row5, "0000000001000010", 0
def row6, "0000000000111100", 0
def row7, "0000000000000000", 0

fct main:

   ;Boot the display.
   cal ebedBoot

   ;Lower the intensity.
   ldi ebedSetIntensity_intensity, 7
   cal ebedSetIntensity

   ;Draw our row.
   ldi ebedSetRow_row, 0
   ldi ebedSetRow_data, row0
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 1
   ldi ebedSetRow_data, row1
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 2
   ldi ebedSetRow_data, row2
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 3
   ldi ebedSetRow_data, row3
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 4
   ldi ebedSetRow_data, row4
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 5
   ldi ebedSetRow_data, row5
   cal ebedSetRow
   
   ;Draw our row.
   ldi ebedSetRow_row, 6
   ldi ebedSetRow_data, row6
   cal ebedSetRow

   ;Draw our row.
   ldi ebedSetRow_row, 7
   ldi ebedSetRow_data, row7
   cal ebedSetRow

   end

;Boots the eight-by-eight display.
fct ebedBoot:
   ;Set the scan limit to 8.
   ldi ebedSetScanLimit_scan_limit, 7
   cal ebedSetScanLimit

   ;Disable decode mode.
   ldi ebedSetDecodeMode_mode, 0
   cal ebedSetDecodeMode

   ;Turn on the display.
   ldi ebedSetShutdownState_state, 1
   cal ebedSetShutdownState

   ;Disable test display.
   ldi ebedSetTestState_state, 0
   cal ebedSetTestState

   ret

;Sets the scan limit for the eight-by-eight display.
def ebedSetScanLimit_code, 2816
def ebedSetScanLimit_scan_limit, 0
fct ebedSetScanLimit:
   add ebedSetScanLimit_scan_limit, ebedSetScanLimit_code
   lod ebedSendWord_word, ebedSetScanLimit_scan_limit
   cal ebedSendWord
   ret

;Sets the decode mode for the eight-by-eight display.
def ebedSetDecodeMode_code, 2304
def ebedSetDecodeMode_mode, 0
fct ebedSetDecodeMode:
   add ebedSetDecodeMode_mode, ebedSetDecodeMode_code
   lod ebedSendWord_word, ebedSetDecodeMode_mode
   cal ebedSendWord
   ret

;Sets the shutdown state for the eight-by-eight display.
def ebedSetShutdownState_code, 3072
def ebedSetShutdownState_state, 0
fct ebedSetShutdownState:
   add ebedSetShutdownState_state, ebedSetShutdownState_code
   lod ebedSendWord_word, ebedSetShutdownState_state
   cal ebedSendWord
   ret

;Sets the test state for the eight-by-eight display.
def ebedSetTestState_code, 3840
def ebedSetTestState_state, 0
fct ebedSetTestState:
   add ebedSetTestState_state, ebedSetTestState_code
   lod ebedSendWord_word, ebedSetTestState_state
   cal ebedSendWord
   ret

;Sets the intensity for the display.
def ebedSetIntensity_code, 2560
def ebedSetIntensity_intensity, 0
fct ebedSetIntensity:
   add ebedSetIntensity_intensity, ebedSetIntensity_code
   lod ebedSendWord_word, ebedSetIntensity_intensity
   cal ebedSendWord
   ret

def ebedSetRow_row, 0
def ebedSetRow_data, 0
def ebedSetRow_word, 0
def ebedSetRow_neg, -1
def ebedSetRow_code, 256
fct ebedSetRow:
   ldi ebedSetRow_word, 0
   
   ;Convert row to appropriate row code.
lbl ebedSetRow_loop:
   add ebedSetRow_word, ebedSetRow_code
   dec ebedSetRow_row
   jne ebedSetRow_loop, ebedSetRow_row, ebedSetRow_neg

   ;Convert row data to a word.
   lod bin2word_bin, ebedSetRow_data
   cal bin2word

   ;Add row data to our word for our final word.
   add ebedSetRow_word, bin2word_word

   ;Send our word.
   lod ebedSendWord_word, ebedSetRow_word
   cal ebedSendWord

   ret

;Sends a word to an eight-by-eight display.
def ebedSendWord_word, 0
def ebedSendWord_pointer, 0
def ebedSendWord_one, "1"
def ebedSendWord_zero, 0
def ebedSendWord_tmp, 0
def ebedSendWord_send, 0
def ebedSendWord_din, 1
def ebedSendWord_clk, 2
def ebedSendWord_cs, 4
fct ebedSendWord:
   set ebedSendWord_zero

   ;Convert the word into a binary string;
   lod word2bin_word, ebedSendWord_word
   cal word2bin
   
   ldi ebedSendWord_pointer, word2bin_bin

lbl ebedSendWord_loop:
   lbr ebedSendWord_tmp, ebedSendWord_pointer
   jpe ebedSendWord_end, ebedSendWord_tmp, ebedSendWord_zero

   ;Check whether or not to send a high or a low bit.
   ldi ebedSendWord_send, 0
   jne ebedSendWord_skip_one, ebedSendWord_tmp, ebedSendWord_one
   add ebedSendWord_send, ebedSendWord_din
lbl ebedSendWord_skip_one:
   set ebedSendWord_send   

   ;Send the bit.
   add ebedSendWord_send, ebedSendWord_clk
   set ebedSendWord_send
   sub ebedSendWord_send, ebedSendWord_clk
   set ebedSendWord_send

   inc ebedSendWord_pointer
   jmp ebedSendWord_loop
lbl ebedSendWord_end:
   ;Send the word.
   add ebedSendWord_send, ebedSendWord_cs
      ldi wait_time, 10000
      cal wait
   set ebedSendWord_send
   sub ebedSendWord_send, ebedSendWord_cs
   set ebedSendWord_send
   ret

;Copies num words of memory from source to memory.
;   Input: memcpy_source (pointer), memcpy_destination (pointer), memcpy_num (immidiate)
;   Output: memcpy_destination (pointer)
def memcpy_source, 0
def memcpy_destination, 0
def memcpy_num, 0
def memcpy_zero, 0
def memcpy_tmp, 0
fct memcpy:
   lbr memcpy_tmp, memcpy_source
   sbr memcpy_tmp, memcpy_destination
   inc memcpy_source
   inc memcpy_destination
   dec memcpy_num
   jne memcpy, memcpy_num, memcpy_zero
   ret

;Converts a binary string into a word.
;   Input: bin2word_bin (pointer)
;   Output: bin2word_word (immidiate)
def bin2word_powers, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0
def bin2word_pointer, 0
def bin2word_word, 0
def bin2word_tmp, 0
def bin2word_bin, 0
def bin2word_zero, 0
def bin2word_one, "1"
def bin2word_negate, -32767
fct bin2word:
   ldi bin2word_pointer, bin2word_powers
   ldi bin2word_word, 0

   ;Check if the sign bit is 1.
   lbr bin2word_tmp, bin2word_bin
   jne bin2word_skip_negative, bin2word_tmp, bin2word_one

   ;Subtract the sign bit if it is 1.
   sub bin2word_word, bin2word_negate
   inc bin2word_word
lbl bin2word_skip_negative:

   inc bin2word_bin
lbl bin2word_loop:

   ;Check if the current bit is 1.
   lbr bin2word_tmp, bin2word_bin
   jne bin2word_loop_skip, bin2word_tmp, bin2word_one
   
   ;Accumulate the power if it is 1.
   lbr bin2word_tmp, bin2word_pointer
   add bin2word_word, bin2word_tmp
lbl bin2word_loop_skip:
   

   ;Increment our pointers.
   inc bin2word_pointer
   inc bin2word_bin

   ;Loop if not the end of the binary string.
   lbr bin2word_tmp, bin2word_bin
   jne bin2word_loop, bin2word_tmp, bin2word_zero

   ret

;Converts a word into a binary string.
;   Input: word2bin_word (immidiate)
;   Output: word2bin_bin (pointer)
def word2bin_word, 0
def word2bin_tmp, 0
def word2bin_char_0, "0"
def word2bin_char_1, "1"
def word2bin_bin, "0000000000000000", 0
def word2bin_powers, 16384, 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0
def word2bin_negate, -32767
def word2bin_one, 1
def word2bin_zero, 0
def word2bin_pointer, 0
def word2bin_pointer_out, 0
def word2bin_power, 0
fct word2bin:
   ;Set the first bit to 1 only if the input is negative.
   lod word2bin_bin, word2bin_char_0
   jge word2bin_positive, word2bin_word, word2bin_zero
   ;If the output is negative, remove the sign bit.
   add word2bin_word, word2bin_negate
   dec word2bin_word
   lod word2bin_bin, word2bin_char_1   
lbl word2bin_positive:
   ldi word2bin_pointer, word2bin_powers
   ldi word2bin_pointer_out, word2bin_bin+1
   ;Loop through the next 7 bits and our powers.
lbl word2bin_loop:
   lbr word2bin_power, word2bin_pointer
   ;Subtract the power from our input.
   lod word2bin_tmp, word2bin_word
   sub word2bin_tmp, word2bin_power
   sbr word2bin_char_0, word2bin_pointer_out
   ;If the result is not less than zero...
   jpl word2bin_loop_skip, word2bin_tmp, word2bin_zero
   ;...then our bit is 1 and store the result back into the input.
   sbr word2bin_char_1, word2bin_pointer_out
   lod word2bin_word, word2bin_tmp
lbl word2bin_loop_skip:
   inc word2bin_pointer
   inc word2bin_pointer_out
   jne word2bin_loop, word2bin_power, word2bin_one
   ret

;Prints a null-terminated string.
;   Input: print_in (pointer)
;   Output: text on the screen
def print_in, 0
def print_char, 0
def print_zero, 0
fct print:
   lbr print_char, print_in
   jpe print_end, print_char, print_zero
   set print_char
   inc print_in
   jmp print
lbl print_end:
   end

;Waits for a certain interval of time.
def wait_time, 0
def wait_time_neg, -1
fct wait:
   dec wait_time
   jne wait, wait_time, wait_time_neg
   ret


As you can see, Rebec can be used to drive hardware. You can see some of my bitwise functions I've been working on here. bin2word and word2bin can convert between binary strings and words (remember, words in this virtual CPU are signed 16-bit numbers).

Eventually I want to build the CPU on an FPGA in Verilog and make the CPU open source as well, but I'm still learning so that will take some time.

Understanding the Virtual Machine from Machine Code

If you're not satisfied just coding in Rebec and love machine code and assembly, I'll try to explain how the virtual machine actually works. It is purposefully designed to be incredible simple in its function, but that makes it incredibly complex when it comes to coding for it. Its simplicity makes porting the virtual machine really easy, and if I ever built this as a real CPU, it's be very cheap to manufacture and incredibly power efficient (as it would have barely any transistors).

But, these benefits come with a big cost, and that is (1) memory, as programs have to be quite large, and (2) difficulty in programming. This is why I created Rebec, because it makes programming this a lot easier. But if you want to understand machine code and assembly, I'll try my best to explain it.

The virtual CPU only has one instruction called "RSSB", which stands for "reverse subtract and skip on borrow". Because the CPU only has one instruction, it does not store store the instruction name in memory. Instead, only the operand of the instruction is stored in memory. And there is only one operand to RSSB, thus every word of memory is a single instruction.

It is a 16-bit, signed, 2s compliment, little Endian CPU.

The RSSB instruction can be summed up like this (this isn't my actual code but it's a nice way to visualize it):


Code:
void rssb(signed short *x) {
  *acc = *x - *acc
  *x = *acc
  if (*acc < 0)
    *pc++;
  otherStuff();
  *pc++;
}


The input "x" value is a pointer that point to a memory address. "pc" and "acc" are also pointers that specifically point to memory addresses 0 and 1 respectively. "pc" is your "program counter" and "acc" is your "accumulator".

It's important to note that pc and acc are NOT external registers. They are simple pointers to memory address 0 and 1, so they themselves refer to locations in RAM. (Technically if you built a physical version of this CPU you could make pc and acc external registers and map them to RAM, but on the programmer's end pc and acc refer to memory addresses.)

When you call "rssb" with some x value, both the accumulator and x are dereferenced and the accumulator is subtracted from x (hence the reverse subtract part). The results are then stored into both the accumulator and x at the same time.

You always want to increment the program counter after every instruction, but if the result of the subtraction was negative, then you increment the program counter an additional time, which skips the next instruction (hence the skip on borrow part).

After the rssb instruction finishes, some "otherStuff()" happens. In this case, I deal with memory mappings to the virtual CPU's RAM. In this case, I map "ZERO" to memory address 2, "POS" to memory address 3, "NEG" to memory address 4, "IN" to memory address 5, "OUT" to memory address 6, and "ROM" to memory address 7.

What are those? "ZERO" always holds the value 0, "POS" always holds the value 1, and "NEG" always holds the value -1. These are refreshed every CPU cycle by "otherStuff()". I put these here because they make programming the CPU a little easier.

"IN" and "OUT" are mapped to the 16 input pins and 16 output pins of the virtual CPU. Every CPU cycle, "otherStuff()" will read whatever data is written in OUT and write it to the output pins. It will then read whatever data is on the input pins and write that to IN.

In the case of the virtual machine on the TI-83+/TI-84+, anytime you write data to "OUT" it will display that to the screen as an ASCII character, and every time you read from it it will pause and allow the user to input text.

"ROM" is the final mapped memory address, it stands for "read-only memory". Its purpose is to allow the CPU to identify itself. ROM stores the part number of the CPU, so the software can read the part number and know what CPU it is. In the virtual machine, it just stores "313".

Now since we know how that works, let's look at a very simple program:


Code:
0000: 0800
0001: 0000
0002: 0000
0003: 0100
0004: FFFF
0005: 0000
0006: 0000
0007: 3901
0008: 1400
0009: 1400
000A: 1400
000B: 1400
000C: 1500
000D: 0200
000E: 0200
000F: 1400
0010: 0100
0011: 0100
0012: 0000
0013: 0000
0014: 0C00
0015: 0F00


What does this program do? It copies the value stored in address 0015 to address 0014. To the 0C00 in 0014 should be erased and be replaced by 0F00.

Understanding the program isn't that hard. Addresses 0-7 are irrelevant. Remember, these are things like your accumulator, your output pins, your input pins, etc. Your actual program starts at memory address 8. The only important thing to know here is that you always want your program counter to start with the value 8 so the CPU will skip over the mapped RAM addresses and will start executing at the beginning of your program. You also want OUT to always start as 0, because that's what the Rebec programming language expects (there's a long reason to why this is I don't want to get into here).

The actual program starts at 0008. Remember, it's little Endian, so the first four instructions are all "rssb(14)". Why 14? 14 is the memory address we want to copy data to.

Thing about it what "rssb(14)" does if we call it just twice...


Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]

rssb(x)
acc[2] = x[1] - acc[1]
x[2] = acc[2]

Expand this out...
acc[2] = x[1] - acc[1]
= [x[0] - acc[0]] - [x[0] - acc[0]]
= 0
x[2] = acc[2]
= 0


So if rssb(x) is called twice, then the memory address that x is pointing to will be cleared (and so will the accumulator).

So why did I call it 4 times instead of 2?

Well, if you're in the middle of a program, calling it twice is bad practice. Because if the value in x is negative, then the CPU will skip over your second rssb(x) and it won't execute. Or, even if it is positive, if you are in the middle of a program, it's possible the last routine that executed ended on a negative, so it'd skip your first instruction.

So calling it 4 times guarantees it will at least be executed twice and will clear out what x is pointing to (in this case, address 0014).

The next instruction is "rssb(15)". Since we know the accumulator was just cleared, what does "rssb(15)" do?


Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]

Substitute...
acc[1] = x[0] - 0
= x[0]
x[1] = x[0]


Effectively, this stores the value the operand is referencing into the accumulator. So, in this case, the accumulator now holds the value at memory address 15. It now holds 0F00.

The next two instructions are "rssb(2)". Remember, that is ZERO, which is a mapped location in RAM that always holds the value of 0. So what would that do?


Code:
rssb(zero)
acc[1] = zero - acc[0]
x[1] = acc[1]

Substitute...
acc[1] = 0 - acc[0]
= -acc[0]
x[1] = acc[1]
= -acc[0]


Calling "rssb(2)" will negate the accumulator. Why do I call rssb(0) twice?

Because if we negate the accumulator, let's assume the number we just loaded is positive, then the second rssb(2) will be skipped since the resulting value in the accumulator is negative, thus we borrowed. Let's assume that we do not skip the next instruction, this means that the value loaded into the accumulator must've been negative. That would mean the first rssb(2) will be skipped.

So, no matter what, either the first or second rssb(2) will be skipped, and no matter what, one or the other will be executed. This guarantees that the accumulator will be negated.

The next instruction is another rssb(14).

Remember, our accumulator now holds the negation of the data stored in memory address 0015, or, -0F00. We also know that memory address 14 is empty. So what does calling rssb(14) at this point do?


Code:
rssb(x)
acc[1] = x[0] - acc[0]
x[1] = acc[1]

Substitute...
acc[1] = 0 - -0F00
= 0F00
x[1] = acc[1]
= 0F00


Now memory address 14 holds 0F00! We did it, we copied the data!

But there's still 4 more lines of code. Why? Because if you just left your program like that, it would through an error when you run it, because you did not halt the CPU properly.

The last four instructions code are "rssb(1)" twice then "rssb(0)" twice. If you remember what 1 and 0 are mapped to, that's "rssb(acc)" and "rssb(pc)" respectively.

How the mapped RAM addresses are setup, these four instructions will always lead to the CPU freezing. Although, this is a very easily detectable state, so I refer to this as the "halting" state. The virtual machine recognizes this state as when to stop the program.

Understanding the Virtual Machine from Assembly Language

Coding for the R313 doesn't have to be done through machine code. It can be done through assembly with the R313 assembler.

The same machine code I wrote above can be rewritten like so:


Code:
main:
   ;Copy y into x.
   rssb x
   rssb x
   rssb x
   rssb x
   rssb y
   rssb zero
   rssb zero
   rssb x

   ;Halt
   rssb acc
   rssb acc
   rssb pc
   rssb pc
   
x:
   rssb 12
   
y:
   rssb 15


Much more beautiful, wouldn't you say?

Notice how I don't define the first 8 words of memory. This is because the assembler does this for you, it always sets OUT to be initially 0 and PC to be initially 8. The other mapped memory addresses you have no control over so those don't matter.

Just like in any other assembly language, "x:" is used for defining symbols, which represent memory locations. For the operand to "rssb", you can also write expressions. Such as, these are all valid:


Code:
   rssb 2+2*2;   The same as rssb 6
   rssb x+1   ;The same as rssb y in this program
   rssb $+2   ;$ is the location of the current instruction


Comments begin with semicolons.

You can learn more about the assembly language by writing programs in Rebec, compiling them, and looking at the output. Rebec compiles to self-commenting assembly code that describes what it is doing.

The virtual machine itself is so simple I might even port it to TI-BASIC if I get bored enough. But the problem is it'd be hella slow. (The GIFs you see are using the TI-84+ with the CPU on its slow setting.)

Some other interesting fact I figured out while working on this

I figured out how to get around the fact "_GetKey" is blocking. If you want to use _GetKey but still want other things to happen on the screen while waiting for a key input (without writing an interrupt, which isn't difficult just unnecessary), simply use "_GetCSC" in a loop until the A register no longer holds 0, then after the loop breaks (when the user presses a key), stick the value of the A register into "(kbdScanCode)", and THEN call "_GetKey".

What that does is after _GetCSC (which is non-blocking) detects you pressed a key, it will then trick the OS to think you pressed the key again so when you call "_GetKey" it will immediately register you pressed the key without blocking. Within the "_GetCSC" loop you can do whatever you want while waiting for a key press, and then you just pass whatever _GetCSC reads into _GetKey.

This is how it's implemented in my code to make a blinking cursor when typing:


Code:
emulateGetInputPrompt:

   ;Display flashing underscore.
   call emulateToggleUnderscore
   call emulateSaveCoordinates
   ld a, (emulateUnderscore)
   bcall(_PutC)
   call emulateLoadCoordinates

   ;Loop while no key is pressed.
   bcall(_GetCSC)
   cp 0
   jp z, emulateGetInputPrompt

   ;Convert pressed key into ASCII.
   ld (kbdScanCode), a
   bcall(_GetKey)
   call keyToASCII


I thought this might be a useful thing. I only figured this out recently.
That's a valuable learning project, and also a good writeup Smile
A single-instruction processor is easy to emulate and implement in hardware, but as you wrote, some more instructions and transistors would proper performance up, ease of use up, binary size down, memory consumption down. For instance, even BrainF* has multiple instructions.

Single-instruction programs remind me of domas' https://github.com/xoreaxeaxeax/movfuscator , based on an outdated compiler (but this can be worked around).

Keep going on Wink
Lionel Debroux wrote:
That's a valuable learning project, and also a good writeup Smile
A single-instruction processor is easy to emulate and implement in hardware, but as you wrote, some more instructions and transistors would proper performance up, ease of use up, binary size down, memory consumption down. For instance, even BrainF* has multiple instructions.

Single-instruction programs remind me of domas' https://github.com/xoreaxeaxeax/movfuscator , based on an outdated compiler (but this can be worked around).

Keep going on Wink


Thanks! I do plan to keep it single-instruction, though. Every computer that exists is multiple instructions. I'm trying to do something different, ya know?

That's part of the challenge here, making a single-instruction computer that's easy to use.

Memory consumption and performance will always be a problem, but it could also be insanely power efficient and incredibly inexpensive.

I'm not really a hardware person, I'm more of a software person, so that's one reason I decided to do this as a side project. The hardware part would be pretty basic, but the software part would be difficult. So I get to write a ton of software while not having to get too much into the weeds of hardware.

I don't plan for this to run the next Crysis or anything. If I can get it to reliably and easily work as a microprocessor to control small projects, even as a toy, that would satisfy my goal.
Quote:
I'm trying to do something different, ya know?

Yes, I noticed Smile
I wish you good luck with that endeavour - as I wrote, it's a great learning project Wink
There is much confusion throughout this project between borrow (opposite of 17-bit sign) and 16-bit sign. Since my arguments on irc didn't appear to have an effect, here is a program exhibiting these issues:
Code:
;Define the data we will be using.
def char, 0
def str_assumption, "If comparisons are supposed to be ", -1
def str_signed, "signed:", 10, -1
def signed_op1, -20000
def signed_op2, 20000
def str_signed_op1, " -20000 ", -1
def str_signed_op2, " 20000 ", -1
def str_unsigned, "unsigned:", 10, -1
def unsigned_op1, 10000
def unsigned_op2, 50000
def str_unsigned_op1, " 10000 ", -1
def str_unsigned_op2, " 50000 ", -1
def str_conclusion, "Either way, comparisons are incorrect QED.", 10, -1
def print_arg, 0

;The main function.
fct main:

; signed tests
   ldi print_arg, str_assumption
   cal print
   ldi print_arg, str_signed
   cal print

; jpl signed < test
   ldi print_arg, str_signed_op1
   cal print
   jpl signed_less, signed_op1, signed_op2
   ldi char, 62
   set char
   ldi char, 61
   jmp signed_notLess
lbl signed_less:
   ldi char, 60
lbl signed_notLess:
   set char
   ldi print_arg, str_signed_op2
   cal print
   ldi char, 10
   set char

; jge signed >= test
   ldi print_arg, str_signed_op2
   cal print
   jge signed_greaterOrEqual, signed_op2, signed_op1
   ldi char, 60
   jmp signed_notGreatorOrEqual
lbl signed_greaterOrEqual:
   ldi char, 62
   set char
   ldi char, 61
lbl signed_notGreatorOrEqual:
   set char
   ldi print_arg, str_signed_op1
   cal print
   ldi char, 10
   set char

; unsigned tests
   ldi print_arg, str_assumption
   cal print
   ldi print_arg, str_unsigned
   cal print

; jpl unsigned < test
   ldi print_arg, str_unsigned_op1
   cal print
   jpl unsigned_less, unsigned_op1, unsigned_op2
   ldi char, 62
   set char
   ldi char, 61
   jmp unsigned_notLess
lbl unsigned_less:
   ldi char, 60
lbl unsigned_notLess:
   set char
   ldi print_arg, str_unsigned_op2
   cal print
   ldi char, 10
   set char

; jge unsigned >= test
   ldi print_arg, str_unsigned_op2
   cal print
   jge unsigned_greaterOrEqual, unsigned_op2, unsigned_op1
   ldi char, 60
   jmp unsigned_notGreatorOrEqual
lbl unsigned_greaterOrEqual:
   ldi char, 62
   set char
   ldi char, 61
lbl unsigned_notGreatorOrEqual:
   set char
   ldi print_arg, str_unsigned_op1
   cal print
   ldi char, 10
   set char

   ldi print_arg, str_conclusion
   cal print
   end

fct print:
   lbr char, print_arg
   set char
   inc print_arg
   lbr char, print_arg
   jnn print, char
   ret
Which when run under rvm outputs:
Code:
If comparisons are supposed to be signed:
 -20000 >= 20000
 20000 < -20000
If comparisons are supposed to be unsigned:
 10000 >= 50000
 50000 < 10000
Either way, comparisons are incorrect QED.
jacobly wrote:
There is much confusion throughout this project between borrow (opposite of 17-bit sign) and 16-bit sign. Since my arguments on irc didn't appear to have an effect, here is a program exhibiting these issues:


This bug has nothing to do with that. It wouldn't make sense for it to be, as it does not matter how you define the word "borrow" because Rebec is based on how the machine actually works, not how you define words. The definition comes from here.

Again, Rebec is built in regards to the virtual machine as it exists. It is built taking into consideration that "borrowing" occurs when you get a negative from a subtraction, meaning the in a - b, b > 0. Calling this "borrowing" or not calling it "borrowing" cannot create bugs. Bugs aren't created from what you call a machine.

The bug has to do with how the current code detects if a number if greater than another number. It effectively works like this:


Code:
short jge(short a, short b) {
   short c = a - b;
   if (c >= 0) return 1;
   else return 0;
}


That seems great on paper but it can get buggy.

-20,000 - 20,000 = -40,000, but -40,000 cannot fit into a word. It underflows and the result of the subtraction is 12,768.

Basically, very large comparisons can fail due to overflow/underflow and I need a better algorithm.

It states a is not less than b even though it clearly is.

Making very large subtractions will simply underflow and give you the wrong result. There's nothing wrong with the virtual machine itself, even Rebec is working as intended, since I specifically wrote the greater-than and less-than functions to behave this way, I just didn't think through that it was a flawed algorithm.

Brooding over this problem for a few hours, I did manage to come up with a solution.

There are four cases when subtracting two numbers (pretend 0s are "positive"):

1. Positive - Positive
2. Negative - Positive
3. Positive - Negative
4. Negative - Negative

The first case cannot produce an underflow under any condition, the fourth should be fine as well. The second and fourth could produce and underflow, the third could produce an overflow.

However, we don't need to worry about case 2 and 3. Because if we are subtracting a positive from a negative, we know already it's that the negative is less than the positive. We don't need to subtract anything. Same with the third, we know just from looking at the signs that the positive is greater than the negative.

So the only case we have left is the 4th. How do we fix it?

Effectively, I have to modify the algorithm to look like this:


Code:
short jge(short a, short b) {
   if (neg(a) && !neg(b)) return 0;
   if (!neg(a) && neg(b)) return 1;
   
   short c = a - b;
   if (c >= 0) return 1;
   return 0;
}


If anyone knows of any other solution to this, please let me know.

Also, as a side note, you don't need to use character codes in Rebec. Just write characters like you do in C. Don't type 60, type '<'.
My point is that if you replace short c = a - b; with int c = a - b; that jge algorithm does work. This is the difference between sign and borrow. The esolang page is completely ambiguous with respect to which way it behaves, just like it is ambiguous with respect to how it handles overflow (in both cases this is something you need to know in order to be able to use the language, but the actual behavior is implementation dependent). Also, I haven't thought of a better way than more or less what you said:
Code:
bool jge(short a, short b) {
    return (short)(a - ((a ^ b) < 0 ? 0 : b)) >= 0;
}


Edit: I was trying to optimize the codegen when I noticed another problem. The rssb zero \ rssb zero trick fails when acc = -32768 since it can't be negated so an extra instruction gets skipped.
jacobly wrote:
My point is that if you replace short c = a - b; with int c = a - b; that jge algorithm does work. This is the difference between sign and borrow.


You are just being ridiculous at this point. It is objectively borrowing by all definitions of the word.

Quote:
The esolang page is completely ambiguous with respect to which way it behaves, just like it is ambiguous with respect to how it handles overflow (in both cases this is something you need to know in order to be able to use the language, but the actual behavior is implementation dependent).


I don't know what you mean. It is not ambiguous, I explained exactly why it gives the results it does and I just need to put aside some time to patch it.

Quote:
Edit: I was trying to optimize the codegen when I noticed another problem. The rssb zero \ rssb zero trick fails when acc = -32768 since it can't be negated so an extra instruction gets skipped.


I currently have the number -32768 in my known bug list. The fact it can't be negated cause minor issues here can there. Such as, if you look up at my binary string algorithm, I subtract the sign bit from the number if it is negative. I do this by adding -32768 to it. But -32768 wouldn't work, because it just becomes 0. So I had to add -32767 to it then decrement.

This is a bug I am aware of but it is rather minor as I have yet to encounter an instance where specifically needing the number -32768 was crucial to an algorithm and there wasn't some easy workaround. I am more concerned about fixing the jumps at the moment and getting something akin to "#include" statements to import libraries.
amihart wrote:
You are just being ridiculous at this point. It is objectively borrowing by all definitions of the word.


Yes I am being ridiculous. I am being ridiculous because it matters as we have seen with the jge case. Since you appear to know some z80, have you ever noticed that the sign flag and carry flag are different flags. That's because they behave differently. To quote a reference you linked (emphasis mine):
https://en.wikipedia.org/wiki/Carry_flag wrote:
A subtract with borrow (SBB) instruction will compute a−b−C = a−(b+C), while a subtract without borrow (SUB) acts as if the borrow bit were clear. The 8080, Z80, 8051, x86 and 68k families (among others) use a borrow bit.
That borrow bit is the carry flag, believe it or not, not the sign flag. You can tell because the sbc instruction uses the carry flag as input instead of the sign flag, and as you have repeatedly mentioned, borrow is used by adc and sbc for multi-precision arithmetic.

I believe that this confusion stems from the fact that most C programmers, not knowing about or having access to assembly flags, and never caring about overflow behavior, just do the thing that works "most of the time", instead of doing something harder and/or slower that only fixes numbers they don't care about. Because when it comes down to it, for 15 bit signed numbers, the sign of a 16 bit arithmetic operation is identical to the borrow of that operation. This is just no longer the case for 16 bit inputs.

Keep in mind that everything I have been saying is functionally identical to what you are saying: that sign and carry are the same thing until overflow is involved.
jacobly wrote:
Yes I am being ridiculous. I am being ridiculous because it matters as we have seen with the jge case.


I still would have to change the jge case. I can change the definition of "borrowing" in the virtual machine, the bug will still remain, because the machine is built consistently with this definition.

If you think it is that important, I will change it as the change is trivial, but the bug in jge and jpl will remain until I write new code for those.

Thanks for your input.
Alright, so the first bug that this change fixes is that rssb zero \ rssb zero now works correctly for all inputs due to 0-(-32768) now correctly not triggering a borrow. The second thing is that it makes signed comparisons fairly easy to implement correctly for all inputs:
Code:
; I am assuming that the first inst always gets executed, otherwise need another dummy rssb temp...
    rssb temp
    rssb temp
    rssb temp
    rssb pos
    rssb temp
    rssb acc
    rssb acc
    rssb z
    rssb temp
    rssb temp
    rssb acc
    rssb y
    rssb neg
    rssb neg
    rssb temp
    rssb acc
; Now the next inst is always executed with y and z preserved, temp destroyed, and acc == 0 if y >= z otherwise acc != 0 if y < z.
; If you swap the rssb z and rssb y insts then you get the same thing except acc == 0 if y <= z otherwise acc != 0 if y > z.
; This now makes it possible to delegate to jpe or jne depending on the comparison.
Oh, nice routine.

I've been working on it and I think the comparisons work now, you can try it yourself if you want to check.


Code:
;Define the data we will be using.
def char, 0
def signed_op1, -20000
def signed_op2, 20000
def str_signed_op1, " -20000 ", -1
def str_signed_op2, " 20000 ", -1
def str_conclusion, "Comparisons are correct QED.", 10, -1
def print_arg, 0

;The main function.
fct main:

; jpl signed < test
   ldi print_arg, str_signed_op1
   cal print
   jpl signed_less, signed_op1, signed_op2
   ldi char, '>'
   set char
   ldi char, '='
   jmp signed_notLess
lbl signed_less:
   ldi char, '<'
lbl signed_notLess:
   set char
   ldi print_arg, str_signed_op2
   cal print
   ldi char, 10
   set char

; jge signed >= test
   ldi print_arg, str_signed_op2
   cal print
   jge signed_greaterOrEqual, signed_op2, signed_op1
   ldi char, 60
   jmp signed_notGreatorOrEqual
lbl signed_greaterOrEqual:
   ldi char, '>'
   set char
   ldi char, '='
lbl signed_notGreatorOrEqual:
   set char
   ldi print_arg, str_signed_op1
   cal print
   ldi char, 10
   set char

   ldi print_arg, str_conclusion
   cal print
   end

fct print:
   lbr char, print_arg
   set char
   inc print_arg
   lbr char, print_arg
   jnn print, char
   ret


I also found a ton of other bugs while working on this that I've fixed and some I'm still working on fixing. There was a pretty significant bug in the evaluator that I fixed. (The evaluator evaluates mathematical expressions.)

One thing I did not mention is that in Rebec you can type out the arguments for commands as mathematical expressions.

Such as,


Code:
def x, 0
fct main:
   ldi x, 2+2*2
   set x
   ldi x, (2+2)*2
   set x
   end
   


If you run the virtual machine in "numeric" mode (which displays the output as numbers rather than ASCII characters, do it by typing the word "numeric" after the file name), you get this as output:


Code:
6
8


You can also type characters with single quotes:


Code:
   ldi x, 'Q'
   set x


Although, characters cannot be part of expressions ('Q' + 2 won't work, for example). This may be something I'll add in the future, though.

You can also type expressions including variables, like:


Code:
   ldi x, y + 3


It's important to note that when using variables in expressions, you don't get the value of the variable. You get its memory address. So "y + 3" would evaluate to the memory address 3 after y.

You can also load a variable's memory address into itself:


Code:
   ldi x, x


I fixed a couple other minor bugs as well.
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are GMT - 5 Hours
 
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

 

Advertisement