/NEU

Primary LanguageGo

Working With This Code

building

You'll need to install ebiten in order to get this code to build. Once you installed that building this code is as simple running make build. This will build both neuBi which is the new byte code assembler, as well as neuVM which is the neu byte code interpreter

testing

Testing this code is as simple as running make test. This will run all the test files in this repo

benchmarks

Benchmarking the code can be done by running make bench. Currently, the only benchmarks are in core/util_bench_test.go. These benchmarks were written to look at the performance differences between I64tob and I64asb (and similar methods for the other int types). Looking at these benchmarks you can see the huge performance gain we get by using unsafe pointers to convert from byte slices into integers and vice versa.

Neu Assembler

The neu text code assembler is called neuBi (pronounced newbie). You can build neuBi by running make build. Once you have neuBi built you can run ./neuBi -h to get more info about how it works. The most common way to use neuBi is to assemble .nb files. Simply run ./neuBi my_file.nb to assemble my_file.nb into my_file.n.

Neu Bytecode and Neu Text Code

Neu is a stackbased bytecode that runs on a small vm which uses a single stack and 1 byte opcodes. Neu text code is the textual representation of neu byte code. Neu text code was designed to be human readable and can be assembled into neu byte code by neuBi Neu text code is stored in .nb files. Neu byte code is stored in .n files in a formt which can be run by the neuVM.

Neu VM

Neu VM is the virtual machine that runs neu byte code. You can build the neuVM by running make build. Once the new VM is built you can run neuVM my_file.n to run your compiled neu byte code. You code will be run by the interpreter 60 times per second and the screen will be updated based on the results of your code.

memory layout

memory layout

When the neuVM starts is allocates a 32k block of memory. This is all the memory your program is allowed to use. The first thing that happens when the VM is started is your code is loaded into this 32k block of memory. The maximum allowed size for you code is 20k. This code is loaded into high memory and will always take up the higest memory address in your code. The neuVM loads this code in such that no empty addresses remain at the end of the memory block. This secion of code is marked as read only and can not be written to. Attempting to write to this region of memory will result in a runtime error.

Once your code has been loaded the neuVM sets up you programs stack. The stack starts at the address where memory changes from read write, to read only. This stack pointer then moves from high memory to low memory as you push items onto the stack. This means you could theorectially write directly to the stack using pop operations. However, this is discourged as pushing and popping from the stack is the default way to perform operations in neu byte code.

Once your code is running you are allowed to write or read from any memory address that is not marked as read only.

the screen

The primary way of showing data to the user is by writing to the screen. The screen is a 64x64 4 color screen which uses the crimson pallet from lospec. This screen is set up to watch the first 1024 bytes in memory (0x00 - 0x400). This region of memory will be read at the end of each frame and updated. Writing data to this region of the memory is the only way to draw onto the screen. The screen uses 2 bit color so each pixel takes up 2 bits in memory. This means the screen is 16 bytes wide (16 bytes * 4 2 bit pixels = 64 pixels) The refresh rate of the screen is 60 times per second (though this may drop if running the byet code becomes too expensive).

Writing Neu Text Code

comments

Comments in neu are specified by two forward slashes //. Anything that comes after these 2 symboles on a line will be ignored by the assembler.

pointers/ memory addresses

Memory addresses can be in either hexidecimal notation (#0x5a, #0x7b) or binary notation (#0b00001110). Pointers are 64 bit unsigned integers. To learn more about about the layout of memory in the neuVM see the section Neu VM about memory layout.

labels

Labels are specified by a name sourounded by brackets for example [label name]. Label names can include numbers, letters and symboles and can be used intercangably with memory addresses. Labels are converted to explicit memory addresses by the assembler.

number literals

Numerical literals can be either in decimal notation (1, 10, 50), hexidecimal notation (0xff, 0x0a), or binary notation (0b00001100). Hex and binary literals must include their respective prefixes to be processed correctly. All numbers are considers signed twos compliment nubers with 2 exceptions

  1. addresses are always considered unsigned 64 bit integers
  2. bytes are always considered unsigned

jump statments

Jump statments come in two flavors, normal jumps or conditional jumps Normal jumps will pop the top 8 bytes off the stack and move to that line in the program and then continue execution from that point. Conditional jumps will first pop the first 2 bytes (or sets of bytes) off the stack and test them agains each other. If the condition returns true it behaves the same as a normal jump. If it returns false the jump address is popped off the stack and discarded and execution continues on to the next instruction.

Op Codes Reference

op codes are 1 byet a pieces and can have 0, 1, or 2 arguments. the stack consists of 1 byte cells and live in the same place as main memory. This table lists all the available op codes, the # indicates an argument which is either a memory address or a literal value. the [L] indicates an argument which is a label to a location in the code.

Name Usage Hex Description
byte add +. 0x00 pop the top two bytes off the stack, add them together and push the result onto the stack
int16 add +o 0x01 pop the top 4 bytes off the stack, convert them to 2 int16s, add them, and push the result onto the stack
int32 add +O 0x02 pop the top 8 bytes off the stack, convert them to 2 int32s, add them, and push the result onto the stack
int64 add + 0x03 pop the top 16 bytes off the stack, convert them to 2 int64s, add them, and push the result onto the stack
byte minus -. 0x04 pop the top two bytes off the stack, subtract them from one another, push the result onto the stack
int16 minus -o 0x05 pop the top 4 bytes off the stack, convert them to 2 int16s, subtract them, and push the result onto the stack
int32 minus -O 0x06 pop the top 8 bytes off the stack, convert them to 2 int32s, subtract them, and push the result onto the stack
int64 minus - 0x07 pop the top 16 bytes off the stack, convert them to 2 int64s, subtract them, and push the result onto the stack
byte multiply *. 0x08 pop the top two bytes off the stack, multipl the first value by the second, push the result onto the stack
int16 multiply *o 0x09 pop the top 4 bytes off the stack, convert them to 2 int16s, multiply them, and push the result onto the stack
int32 multiply *O 0x0a pop the top 8 bytes off the stack, convert them to 2 int32s, multiply them, and push the result onto the stack
int64 multiply * 0x0b pop the top 16 bytes off the stack, convert them to 2 int64s, multiply them, and push the result onto the stack
byte divide /. 0x0c pop the top two bytes off the stack, divide the first value by the second, push the result onto the stack
int16 divide /o 0x0d pop the top 4 bytes off the stack, convert them to 2 int16s, divide them, and push the result onto the stack
int32 divide /O 0x0e pop the top 8 bytes off the stack, convert them to 2 int32s, divide them, and push the result onto the stack
int64 divide / 0x0f pop the top 16 bytes off the stack, convert them to 2 int64s, divide them, and push the result onto the stack
byte push <. # 0x10 push a new byte onto the stack, # must be a literal
int16 push <o # 0x11 push 2 new bytes onto the stack as an int16, # must be a literal
int32 push <O # 0x12 push 4 new bytes onto the stack as an int32, # must be a literal
int64 push < # 0x13 push 8 new bytes onto the stack as an int64, # must be a literal
byte pop >. 0x14 pop the top 9 byte off the stack and writes the last byte to the memory address in the first 8 bytes
int16 pop >o 0x15 pop the top 10 byte off the stack and writes the last 2 bytes to the memory address in the first 8 bytes
int32 pop >O 0x16 pop the top 12 byte off the stack and writes the last 4 bytes to the memory address in the first 8 bytes
int64 pop > 0x17 pop the top 16 byte off the stack and writes the last 8 bytes to the memory address in the first 8 bytes
bitwise or |. 0x18 pop the top two bytes off the stack, bitwise or's them together and push the result onto the stack
int16 bit or |o 0x19 pop the top 4 bytes off the stack, convert them to 2 int16s, or them together, and push the result onto the stack
int32 bit or |O 0x1a pop the top 8 bytes off the stack, convert them to 2 int32s, or them together, and push the result onto the stack
int64 bit or | 0x1b pop the top 16 bytes off the stack, convert them to 2 int64s, or them together, and push the result onto the stack
bitwise and &. 0x1c pop the top two bytes off the stack, bitwise and's them together and push the result onto the stack
int16 bit and &o 0x1d pop the top 4 bytes off the stack, convert them to 2 int16s, and them together, and push the result onto the stack
int32 bit and &O 0x1e pop the top 8 bytes off the stack, convert them to 2 int32s, and them together, and push the result onto the stack
int64 bit and & 0x1f pop the top 16 bytes off the stack, convert them to 2 int64s, and them together, and push the result onto the stack
bitwise xor ^. 0x20 pop the top two bytes off the stack, bitwise xor's them together and push the result onto the stack
int16 bit xor ^o 0x21 pop the top 4 bytes off the stack, convert them to 2 int16s, xor them together, and push the result onto the stack
int32 bit xor ^O 0x22 pop the top 8 bytes off the stack, convert them to 2 int32s, xor them together, and push the result onto the stack
int64 bit xor ^ 0x23 pop the top 16 bytes off the stack, convert them to 2 int64s, xor them together, and push the result onto the stack
bitwise left shift <<. 0x24 pop the top byte off the stack as count, shift the next byte count times to the left
int16 bit left shift <<o 0x25 pop the top byte off the stack as count, shift the next int16(2 bytes) count times to the left
int32 bit left shift <<O 0x26 pop the top byte off the stack as count, shift the next int32(4 bytes) count times to the left
int64 bit left shift << 0x27 pop the top byte off the stack as count, shift the next int64(8 bytes) count times to the left
bitwise right shift >>. 0x28 pop the top byte off the stack as count, shift the next byte count times to the right
int16 bit right shift >>o 0x29 pop the top byte off the stack as count, shift the next nt16(2 bytes) count times to the right
int32 bit right shift >>O 0x2a pop the top byte off the stack as count, shift the next nt32(4 bytes) count times to the right
int64 bit right shift >> 0x2b pop the top byte off the stack as count, shift the next nt64(8 bytes) count times to the right
jump if greater ?>. 0x2c jump the execution pointer to the memory address on the stack if the top byte on the stack is larger than the second byte
int16 jump if greater ?>o 0x2d jump the execution pointer to the memory address on the stack if the top int16 on the stack is larger than the second int16
int32 jump if greater ?>O 0x2e jump the execution pointer to the memory address on the stack if the top int32 on the stack is larger than the second int32
int64 jump if greater ?> 0x2f jump the execution pointer to the memory address on the stack if the top int64 on the stack is larger than the second int32
jump if less ?<. 0x30 jump the execution pointer to the memory address on the stack if the top byte on the stack is smaller than the second byte
int16 jump if less ?<o 0x31 jump the execution pointer to the memory address on the stack if the top int16 on the stack is smaller than the second int16
int32 jump if less ?<O 0x32 jump the execution pointer to the memory address on the stack if the top int32 on the stack is smaller than the second int32
int64 jump if less ?< 0x33 jump the execution pointer to the memory address on the stack if the top int64 on the stack is smaller than the second int32
jump > 0x34
byte mod %. 0x35 pop the top two bytes off the stack, mod the first value by the second, push the result onto the stack
int16 mod %o 0x36 pop the top 4 bytes off the stack, convert them to 2 int16s, mod them, and push the result onto the stack
int32 mod %O 0x37 pop the top 8 bytes off the stack, convert them to 2 int32s, mod them, and push the result onto the stack
int64 mod % 0x38 pop the top 16 bytes off the stack, convert them to 2 int64s, mod them, and push the result onto the stack
push byte 0 <0. 0x39 push a zero byte onto the stack
push int16 0 <0o 0x3a push a zero int16 onto the stack
push int32 0 <0O 0x3b push a zero int32 onto the stack
push int64 0 <0 0x3c push a zero int64 onto the stack
dec byte --. 0x3d pop the top byte off the stack subtract one and push the result back onto the stack
dec int16 --o 0x3e pop the top 2 bytes off the stack, convert them to an int16, subtract one and push the result onto the stack
dec int32 --O 0x3f pop the top 4 bytes off the stack, convert them to an int32, subtract one and push the result onto the stack
dec int64 -- 0x40 pop the top 8 bytes off the stack, convert them to an int64, subtract one and push the result onto the stack
inc byte ++. 0x41 pop the top byte off the stack, add one and push the result onto the stack
inc int16 ++o 0x42 pop the top 2 bytes off the stack, convert them to an int16, subtract one and push the result onto the stack
inc int32 ++O 0x43 pop the top 4 bytes off the stack, convert them to an int32, subtract one and push the result onto the stack
inc int64 ++ 0x44 pop the top 8 bytes off the stack, convert them to an int64, subtract one and push the result onto the stack
byte push <#. 0x45 pop the top uint64 off the stack as an address, push the byte at that address onto the stack
int16 push <#o 0x46 pop the top uint64 off the stack as an address, push the int16 at that address onto the stack
int32 push <#O 0x47 pop the top uint64 off the stack as an address, push the int32 at that address onto the stack
int64 push <# 0x48 pop the top uint64 off the stack as an address, push the int64 at that address onto thte stack
break point (/) 0x49 break point for debugging
byte duplicate stack X2. 0x4a push the top byte onto the stack again
int16 duplicate stack X2o 0x4b push the top int16 onto the stack again
int32 duplicate stack X2O 0x4c push the top int32 onto the stack again
int64 duplicate stack X2 0x4d push the top int64 onto the stack again
label [L] ---- label marks a section of the code.
name address _ = # ---- specifiy a name for a numerical constant that can be used later in your code
memory address # ---- converts a numerical literal into a memory address

Debugger

The neu interpreter comes with a debugger built in to make it easier to write your code. The breakpoint symbole is (/), and will halt execution of you program and start the debugger. To get more information on the debugger simply type ? or help and you will be given a list of debugger commands. Remember that your code will run every frame so you code may halt every frame depending on where you put your break point. Generally the debugger presents its data as hex numbers but occationally it shows them as decimal numbers as well. When this is the case the decimal number is printed first followed by the | symbole and then the hex number with an explicit hex prefix 0x For example the spt or stack pointer address command will return [ 100 | 0x00000064 ].

Example Programs

example 1

this is a simple program for adding the numbers 5 and 10 together

0: <. 5
1: <. 10
2: +.

the line numbers are only here for reference and are not present in an actual program. The first line defines the size of the stack. The stack is not allowed to grow past this size. Here the max size is defined as two. Line 0 pushes the literal value 5 onto the stack with the push opcode(<). Line 1 pushes the literal value 10 onto the stack in the same way. Line 2 addes the top pops the top two numbers off of the stack, adds them together and then pushes the result back onto the stack. Note the poping a value off the stack does not clear the value, it simply moves the stack pointer. Thus the final state of the stack after this progam runs is:

00000010 | 10 | 
00000011 | 15 | < stack pointer

drawing to the screen

drawing to the screen is as simple as writing to the correct location in memory. Pixels are 2 bits each and there are 4 pixels packed into each byte

       pxl1 pxl2 pxl3 pxl4
byte: [ 00   11   01   10 ]

You can see an example of how to draw to the screen here

hello world

Neu byte code is very low level and bare bones. This mean writing a hello world example is not as simple as it might be in a higher level language like python or even c. You can see an example of a hello world program here. This program starts by loading a custom font into memory. font reference

Once this is done the code uses 3 "functions" to write the text 'HELLO WORLD!' to the screen. While these are not true functions in the strict sense of the word the work in a somewhat similar manner so I use the terminology moving forward. The first is a function to draw strings to the screen. The second is a function that draws individual characters to the screen which the PrintString function calls on a loop. The final function draws a row of pixels from a character, this is called in a loop from the PrintChar function. Together these 3 functions write the text to the screen.

Future Goals

I've put this code away for now but there are a few features I plan to add if I get the change to return to it.

  • a frame counter in memory so there can be frame based logic in the code
  • memory addresses for getting keyboard input kind of like the screen. The VM would update the specified memory addresses to indicate what buttons were pressed on a given frame so code could recieve some outside input.
  • memory addresses for getting mouse input. This would work the same as the keyboard input but would be for the mouse instead.
  • allowing code to define it's own pallet when it's loaded into the VM (maybe even allow it to be changed at runtime).
  • some byte codes for working with arrays

If you are interested in writing any of these features yourself feel free to open a PR with the new feature. Email me at opensource.atkin@gmail.com for a review and merge.