HMN Learning Jam 2024 (Initial Commit)
Not all instructions are implemented in VM. Only Copy, Add and Halt.
This commit is contained in:
commit
53f8ba27fb
|
@ -0,0 +1,2 @@
|
|||
build/
|
||||
debug/
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
test -d build/ || mkdir -p build
|
||||
|
||||
# GepVM
|
||||
gepvm/build.sh
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
# The Pinnochio Executable Format
|
||||
**Required Reading: *[Memory Layout in Gepetto](mem.md)***
|
||||
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
# Gepetto and IO
|
||||
**Required Reading: *[Memory Layout in Gepetto](mem.md)***
|
|
@ -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).
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
test -d build/gepvm || mkdir -p build/gepvm
|
||||
pushd build/gepvm
|
||||
|
||||
odin build ../../gepvm -debug
|
||||
|
||||
popd
|
|
@ -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(®isters.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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Reference in New Issue