HMN Learning Jam 2024 (Initial Commit)

Not all instructions are implemented in VM. Only Copy, Add and Halt.
This commit is contained in:
unknown 2024-03-25 04:33:09 +00:00
commit 53f8ba27fb
11 changed files with 353 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
debug/

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# The Gepetto Virtual Machine
...is a generic virtual machine designed to be a simple abstraction layer of
minimal operating systems. See [here](https://midnadimple.bearblog.dev/ideal-comp-env)
for the big picture.
While it is intended to run on minimal operating systems that are specialised
to hardware, a reference implmentation for Windows, MacOS and Linux is included.
This reference implementation includes a virtual machine and an assembler is
fast, easily extensible by the end user and allows you to bundle the VM with
your program's bytecode for distribution.
There's also plans to add a dedicated Odin-like programming language for ease
of development.
To build all the tools, run `./buildall.sh` in a Bash shell.
See:
- [Memory and Registers](doc/mem.md)
- [A tutorial for the assembly language](doc/asm.md)
- [All instructions on the machine](doc/inst.md)
- [I/O in the machine](doc/io.md)
- [The Pinnochio Executable Format](doc/exe.md)
- [Livestreams of development](https://youtube.com/@midnadimple)

6
buildall.sh Normal file
View File

@ -0,0 +1,6 @@
#!/bin/bash
test -d build/ || mkdir -p build
# GepVM
gepvm/build.sh

47
doc/asm.md Normal file
View File

@ -0,0 +1,47 @@
# A tutorial on the assembly language
**Required Reading: *None***
Gepetto executables are assembled from `.gepe` source files, which are
sequential lists of mnemonics and their arguments.
Here's an example of assembly:
```
{ main
move l0 9
add l0 29
store l0, 0xf1
}
```
Whitespace before the first character on a line is ignored, whitespace after a
character seperates tokens. (i.e. `{` and `main` are seperate tokens). Note that
any character can be used fo mnemonics, though most are alphanumeric. Mnemonics
can also have any length >= 1.
You can include other `.gepe` files:
```
%include "../test.gepe"
```
Gepetto executables are intended to be compiled as single translation units.
The reference implementation allows user-defined macros, which are simple string
substitutions. A number of arguments can be specified and indexed within
the macro:
```
%macro coolio 2
move %1 %2
add %1 %2
%endmacro
```
There is also a set of standard macros provided with the reference assembler to
improve your development experience. They can be accessed using includes:
```
%include "std.gepe"
```
The search path for source files is, in order:
- Path where assembler was run (source directory)
- Path where assembler was compiled (runtime directory)
- Path specified in assembler args

3
doc/exe.md Normal file
View File

@ -0,0 +1,3 @@
# The Pinnochio Executable Format
**Required Reading: *[Memory Layout in Gepetto](mem.md)***

13
doc/inst.md Normal file
View File

@ -0,0 +1,13 @@
# The Gepetto Instructions Reference
**Required reading: *[Memory Layout in Gepetto](mem.md)*, *[A tutorial on the assembly language](asm.md)***
Maximum of 256 instructions. Opcode = 8bits
- Arithmetic
- Control Flow
- Data Flow
- Copy
- Copy Register to Register
- Copy Literal to Register
- Copy Memory to Register
- Copy Register to Memory

2
doc/io.md Normal file
View File

@ -0,0 +1,2 @@
# Gepetto and IO
**Required Reading: *[Memory Layout in Gepetto](mem.md)***

45
doc/mem.md Normal file
View File

@ -0,0 +1,45 @@
# Memory Layout in Gepetto
**Required Readed: *None***
Gepetto uses a 64-bit address space, which allows for a theoretical maximum of
2^64 bytes of memory. The practical maximum is dependent on the implementation.
The reference implementation uses Odin's `[dynamic]u64`, which can allocate
more memory when needed. By default, 2 times the size of the executable is
reserved.
There are 2 classes of registers in Gepetto, all of which are 64-bit. These are:
## Global Registers
...are visible throughout the duration of the program. These include:
- `gi` = Instruction Pointer, holds the address to the next instruction in
memory. Initialized to 0. Encoded as `0000`
- `gs` = Stack Pointer, holds the address at the top of the stack. Encoded as `0001`
Initialized to 0, so must be set before using stack operations.
- `gb` = Base Pointer, holds the address for displacement and bottom stack. Encoded as `0010`
Initialized to 0, so must be set before using stack operations.
- `gr` = Return Register, intended to store the returned values of
subroutines. Initialized to 0. Encoded as `0011`
- `gl` = Local Pointer, explained in the next section. Initialized to 0. Encoded as `0100`
## Local Registers
In Gepetto, there is technically an *infinite* amount of registers (the
reference implementation uses Odin's `[dynamic]u64`). However, for the
sake of simplicity, only 16 are visible at a time. Those are:
- `l[0-9]` = 10 General-Purpose Registers, all initialized to 0. l0 is encoded
as `0101` with each register after incrementing.
- `lf` = Flag Register, updated by conditional branch branch instructions. The
following table explains the bit layout of the register. Initialized to 0. Encoded as `1111`
**TODO**
Which registers are used is dependent of the `gl` register. It acts as an
offset into this array. Each register's index is calculate by adding the value
in `gl` to the number of the local register. So, `l9` =
`register[9 + register[GL]]`, for example.
This means, if you need more registers, you can simply increment `gl` by 10, and
you have a fresh new set of registers. You can also increment `gl` by a smaller
value, if you only need a few more. The reference implementation provides the
macro `newreg`, which calls `add gl, 10`, and `{`, which calls `newreg` and
defines a label (giving you a new scope/subroutine).

8
gepvm/build.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
test -d build/gepvm || mkdir -p build/gepvm
pushd build/gepvm
odin build ../../gepvm -debug
popd

166
gepvm/gepvm.odin Normal file
View File

@ -0,0 +1,166 @@
package gepvm
import "./registers"
import "core:fmt"
memory: [dynamic]u64
Opcode_Type :: enum {
Copy,
Add,
Halt,
}
sign_extend :: proc(x, bit_count: u64) -> u64 {
result := x
if ((result >> (bit_count - 1)) & 1) != 0 {
result |= 0xFFFFFFFFFFFFFFFF << bit_count
}
return result
}
main :: proc() {
// TODO: Load file into memory
resize(&memory, 1024)
defer delete(memory)
resize(&registers.registers, 1024)
defer delete(registers.registers)
memory[0xFF] = 255
memory[0x0] = 0x0080000000000FF5 // copy l0 [0xFF]
memory[0x1] = 0x0000000000000065 // copy l1 l0
memory[0x2] = 0x0100000000000065 // add l1 l0
memory[0x3] = 0x01800000000000F6 // add l1 15
memory[0x4] = 0x00C0000000000FF6 // copy [0xFF] l1
memory[0x5] = 0x0200000000000000 // halt
// Loop over instructions
running := true
for running {
// Get The opcode
inst := memory[registers.get(registers.GI)]
opcode := Opcode_Type(inst >> 56)
// Execute 'em
switch opcode {
case .Copy: {
type_of_copy := (inst >> 54) & 0x3
switch type_of_copy {
// Register To Register
case 0x0: {
dest_index := (inst >> 4) & 0xF
src_index := inst & 0xF
src_val := registers.get(src_index)
when ODIN_DEBUG {
fmt.printf("Copying register %d with value %d to register %d\n", src_index, src_val, dest_index)
}
registers.set(dest_index, src_val)
}
// Literal to Register
case 0x1: {
lit_50 := (inst >> 4) & 0x3FFFFFFFFFF
dest_index := inst & 0xF
lit_50 = sign_extend(lit_50, 50)
when ODIN_DEBUG {
fmt.printf("Copying literal %d to register %d\n", lit_50, dest_index)
}
registers.set(dest_index, lit_50)
}
// Memory to Register
case 0x2: {
disp_50 := (inst >> 4) & 0x3FFFFFFFFFF
dest_index := inst & 0xF
mem_index := registers.get(registers.GB) + disp_50
value := memory[mem_index]
when ODIN_DEBUG {
fmt.printf("Copying value %d at memory address %d to register %d\n", value, mem_index, dest_index)
}
registers.set(dest_index, value)
}
// Register to Memory
case 0x3: {
disp_50 := (inst >> 4) & 0x3FFFFFFFFFF
src_index := inst & 0xF
index := registers.get(registers.GB) + disp_50
value := registers.get(src_index)
when ODIN_DEBUG {
fmt.printf("Copying register %d with value %d to memory address %d\n", src_index, value, index)
}
memory[index] = value
}
case: {
when ODIN_DEBUG {
fmt.println("Invalid Copy, skipping")
}
}
}
}
case .Add: {
type_of_add := (inst >> 55) & 0x1
switch type_of_add {
// Register and Register
case 0x0: {
dest_index := (inst >> 4) & 0xF
src_index := inst & 0xF
dest_val := registers.get(dest_index)
src_val := registers.get(src_index)
result := dest_val + src_val
when ODIN_DEBUG {
fmt.printf("Adding register %d with value %d to register %d with value %d, result is %d\n",
src_index, src_val, dest_index, dest_val, result)
}
registers.set(dest_index, result)
}
// Literal and Register
case 0x1: {
lit_51 := (inst >> 4) & 0x7FFFFFFFFFF
dest_index := inst & 0xF
lit_51 = sign_extend(lit_51, 51)
dest_val := registers.get(dest_index)
result := dest_val + lit_51
when ODIN_DEBUG {
fmt.printf("Adding %d to register %d with value %d, result is %d\n", lit_51, dest_index, dest_val, result)
}
registers.set(dest_index, value)
}
case: {
when ODIN_DEBUG {
fmt.println("Invalid Add, skipping")
}
}
}
}
case .Halt: {
when ODIN_DEBUG {
fmt.println("Halting")
}
running = false
}
case: {
when ODIN_DEBUG {
fmt.println("Invalid Instrction, skipping")
}
}
}
registers.set(registers.GI, registers.get(registers.GI) + 1)
}
}

View File

@ -0,0 +1,38 @@
package registers
registers: [dynamic]u64
GI :: 0
GS :: 1
GB :: 2
GR :: 3
GL :: 4
L0 :: 5
L1 :: 6
L2 :: 7
L3 :: 8
L4 :: 9
L5 :: 10
L6 :: 11
L7 :: 12
L8 :: 13
L9 :: 14
LF :: 15
get :: proc(index: u64) -> u64 {
if index <= GL {
return registers[index]
}
else {
return registers[index + GL]
}
}
set :: proc(index, value: u64) {
if index <= GL {
registers[index] = value
}
else {
registers[index + GL] = value
}
}