From 0c3a290ce85cfb32b1f85a9274a16b61cd5edaf2 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 6 Feb 2023 12:20:03 -0600 Subject: [PATCH] Basics page --- vweb/build.sh | 2 +- vweb/src/basics.html | 538 ++++++++++++++++++++++++++++++++++ vweb/src/snippet_emulator.css | 17 +- 3 files changed, 553 insertions(+), 4 deletions(-) create mode 100644 vweb/src/basics.html diff --git a/vweb/build.sh b/vweb/build.sh index 0294527..2826551 100755 --- a/vweb/build.sh +++ b/vweb/build.sh @@ -2,4 +2,4 @@ wasm-pack build --target web npm run bundle -cp pkg/vweb_bg.wasm src/index.html src/snippet_emulator_demo.html build +cp pkg/vweb_bg.wasm src/index.html src/snippet_emulator_demo.html src/basics.html build diff --git a/vweb/src/basics.html b/vweb/src/basics.html new file mode 100644 index 0000000..693250f --- /dev/null +++ b/vweb/src/basics.html @@ -0,0 +1,538 @@ + + + + + Vulcan Basics + + + + + +
+

What is Vulcan?

+ +

+ Vulcan is an imaginary computer: it doesn't (yet) exist as a real chip, but there are a couple emulators which allow + other computers to run Vulcan programs. Vulcan programs are a sequence of numbers stored in memory; each number is + an instruction and the computer runs them in sequence to manipulate the state of the machine. Some memory locations + in Vulcan are tied to (emulated) devices like a monitor, mouse, keyboard, and mass storage, so reading or writing + those locations allows Vulcan to interact with the outside world. +

+ +

+ Writing programs as a sequence of numbers is obviously very convenient and easy, but I've written some tools anyway: + the first is an assembler which will translate textual names of instructions into the numbers they correspond to (as + well as do some other bookkeeping). Because even writing assembly language can be tedious, there is also a + high-level language called NovaForth that runs on Vulcan itself, and a different high-level language which runs on a + normal computer but compiles programs which run on Vulcan. +

+ +

Vulcan is a stack machine

+ +

+ The model of computation Vulcan uses is called a stack machine, because instructions manipulate a stack of + values in memory and most data transfer in the machine is done through that stack. There are 42 Vulcan instructions, + each of which might consume values from the data stack or leave other values on top of the stack. Each instruction + can also take a single optional argument, which is pushed to the stack before the instruction runs. +

+ + + +

First program

+ +

+ Here's a short example program: +

+ +
+
+push 2
+push 3
+add
+hlt
+    
+
+ +

+ Going line-by-line through this program: the first instruction is a push instruction with + an argument of 2. The push has no actual effect on the machine + (in fact, it's another name for the instruction nop, for "no operation"). But, the fact + that it has an argument means that the argument will be added to the top of the stack, so, after this line, the + stack contains a 2. The second instruction is the same, and pushes its argument on top of + the 2, so after that line the stack contains 2, 3. The next instruction, + add, has no argument, but the function of the instruction is to remove the top two values + from the stack and push their sum in their place: now the stack contains 5. Finally the + hlt instruction (short for "halt") tells the machine to stop running. +

+ + + +

Instruction encoding

+ +

+ In memory, this program is represented by six bytes: two bytes each for the push + instructions, one byte each foo add and hlt. Vulcan instructions + are always between one and four bytes long: +

+ +
Instruction     Argument bytes
+     |          |            |
+     v          v            v
+  |------##|--------|...|--------|
+         ^
+         |
+     Number of
+   argument bytes
+ +

+ The higher six bits of the first byte determine which instruction it is, and the lower two bits determine how many + bytes the argument is: 0-3 bytes, because a standard memory cell in Vulcan is 24 bits (3 bytes) long. The + instruction push 2 is therefore two bytes long, 0x0102: +

+ +
00000001 00000010
+ +

+ The instruction 0 is push (or nop), it + has one byte of argument, and that one byte is the number 2. The entire program above is: + 1, 2, 1, 3, 4, 116. +

+ +

+ Because any instruction can take an argument, which is treated as a push, we can shorten this program by one + instruction: +

+ +
+
+push 2
+add 3
+hlt
+    
+
+ +

+ Any time there's a push followed by an instruction with no argument, we can remove the + push and move its argument to the following instruction. This makes Vulcan programs somewhat easier to understand + than otherwise, and also saves some memory; this program is only five bytes long: + 1, 2, 5, 3, 116 +

+ +

Memory layout

+ +

+ Properly, each of these programs should start with a directive telling the assembler where they should go in memory. + The default location is fine for these examples (they will run the same way no matter where they are) but we can + place them somewhere with a .org (short for "origin") directive: +

+ +
+
+.org 0x400
+push 40
+mul 40
+hlt
+    
+
+ +

+ The default Vulcan memory map looks like this: +

+ + + +

+ The first 256 bytes of memory are reserved for I/O control registers: reading and writing these may affect devices + like the screen, serial device, keyboard controller, etc. Not all are used, but you shouldn't use them in your + program because someday they might be. +

+ +

+ The next 768 bytes are the stack: since Vulcan uses a word size of 24 bits, stack cells are 24 bits long, + which means the stack is 256 cells long. It can be moved around or made smaller or larger using the + setsdp instruction, but that’s where it starts. +

+ +

+ Immediately after the stack is location 1024, or 0x400: this is where your program is + initially loaded, and when the machine is powered on (or reset) this is where it will start reading and executing + instructions. +

+ +

+ Everything after that is available for use by your program, up to a usual total memory size of 128k... Although for + programs that use video, a large portion of that memory is used by the video display. +

+ +

The call stack

+ +

+ The Vulcan stack isn't just one stack, there are two: the basic one we've already seen is the data stack, + growing upward from location 256, but there is also a call stack (sometimes called a return stack + or R stack) growing downward from location 1023. Here’s an example program that uses both stacks: +

+ +
+
+.org 0x400
+push 2 ; Push a couple values to the stack
+push 3
+pushr ; Move the one on top to the other stack
+pop ; Remove the 2…
+popr ; Move the 3 back
+hlt
+    
+
+ +

+ This puts a couple values on the stack and then deletes the one second-from-the-top, by first moving the top value + to the other stack to get it out of the way. The pushr and popr + instructions move the top value from the data to the call stacks and back again, respectively; + pop removes the top value from the data stack. +

+ +

+ Just having a second stack for temporary storage is somewhat handy, but the actual purpose of the call stack is the + call and ret instructions: when you execute a + call Vulcan stores the address of the next instruction in the call stack and starts + running code at a different address (whatever is on top of the stack). A ret does the + opposite; pops an address from the call stack and goes back to running code there. We can use + call and ret to make a subroutine and call it repeatedly: +

+ +
+
+.org 0x400
+push 1
+call triple
+call triple
+call triple
+hlt
+
+triple:
+mul 3
+ret
+    
+
+ +

+ There are a few new concepts in this snippet, so we’ll tackle them one-by-one: first, the line + triple: is called a label; it doesn't correspond to an instruction, it just tells + the assembler to give a name to a particular location in the program so we can refer to it elsewhere. Whatever the + memory address of the next instruction is, whenever we write triple in our program, it + will be replaced with that address. +

+ +

+ The next instruction is mul 3, which does exactly what it sounds like: just like + add, mul pops two values and pushes their product. +

+ +

+ Which leaves the call and ret lines: we're pushing a 1 and + calling triple three times; each call will jump the execution to the + mul 3 line, then to the ret line, which will jump to the + location after each call. After tripling a 1 three times we're left with a 27 on the stack and we halt. +

+ +

+ In terms of a higher level programming language, triple could be a function that takes a + single number as a parameter and returns that number times 3: +

+ +
function triple(x) {
+  return x * 3
+}
+ +

Branching and conditionals

+ +

+ The true power of any computer is its ability to change its operation based on its input: to make decisions; do one + thing with one piece of data and a different thing with another. Vulcan has instructions to do this: + brz and brnz are short for "branch if zero" and "branch if + non-zero," and will move to running a different part of the program depending on whether the top-of-stack value is + zero or not. Here’s an example program: +

+
+
+.org 0x400
+push 1
+call collatz
+push 2
+call collatz
+hlt
+
+collatz:
+dup
+mod 2
+brz @halve
+mul 3
+add 1
+ret
+halve:
+div 2
+ret
+    
+
+ + + +

+ The first part of this program is pretty basic: call the collatz function on the numbers 1 + and 2. Where it gets interesting is what that function does: first it uses dup (short for + "duplicate," pushes another copy of the top-of-stack to the stack) and mod 2 (short for + "modulus," returns the remainder when two numbers are divided) to tell whether the number is odd or even: an even + number will leave a 0 on the stack, and odd number a 1. +

+ +

+ Then we use brz to branch to halve if there's a zero (if the + number is even). It's important to note the @ in front of + halve, the brz instruction takes a relative address, and it's a + common mistake to leave off that operator and instead use an absolute address. +

+ + + + + +

+ Whether it actually goes to the other branch or not, brz consumes the zero-or-one from the + stack. If it was a 1, then we just continue on this branch: multiply by 3, add 1, return. If it was a 0, then we + skip those three lines, jumping straight to halve, and divide by 2 and return. +

+ +

+ We can also use this to write loops: +

+ +
+
+.org 0x400
+push 1
+loop:
+dup
+add 1
+dup
+lt 10
+brnz @loop
+hlt
+    
+
+ +

+ This will leave the numbers 1-10 on the stack: we start with a 1, duplicate it and increment it, and then check if + the result is less than 10 (lt, "less than," consumes two numbers and pushes a 1 if the + second is less than the first, 0 otherwise). If it is, we branch back to the loop label + and do it again; otherwise halt. Note that we have to dup the number before the + lt 10 check because lt consumes both numbers: we need a copy to + save on the stack and a copy for lt to consume! +

+ +

Variables and memory

+ +

+ So far all our programs have used the stack to store their data, but of course we have 128k of memory we can store + whatever we want in. Here’s a program that puts some of that to good use: +

+ +
+
+.org 0x400
+push 1
+loop:
+dup
+loadw sum
+add
+storew sum
+add 1
+dup
+lt 101
+brnz @loop
+pop
+loadw sum
+hlt
+sum: .db 0
+    
+
+ +

+ Starting with the last line first: the .db directive is short for "define byte" and, like + .org, doesn't represent an instruction: it just causes the assembler to put three bytes + containing whatever value we want into the program. It needs to go at the end because if it were at the start + Vulcan would try to execute it as code (which in this case would actually be safe, because three bytes of 0 is just + three nop instructions, but still). +

+ +

+ Having reserved a memory location for our variable and given it a name (the label sum) we + can do a loop much like the previous example: load the value from sum, add the top of the + stack to it, store it back into sum, increment the top of the stack, repeat for every + number between 1 and 100. Finally throw away the now-useless counter variable and load sum + one last time, leaving the sum of the numbers 1-100 (5050) on top of the stack. +

+ +

+ Vulcan has four instructions for reading and writing arbitrary memory: +

+ + + +

+ Most variables in Vulcan are one word long; reading and writing single bytes is usually used for manipulating + individual characters in strings. +

+ +

Assembler macros

+ +

+ Certain structures in assembly are very common to write, like an if / else condition or a loop. In order to prevent + errors writing these the Vulcan assembler provides several macros that generate these common structures for + us. Importantly, these aren't new instructions, they're commands to the assembler to generate sequences of + instructions (and single-use labels) for us. +

+ +

+ The simplest of these is the #if macro: +

+ +
+
+.org 0x400
+push 10
+#if
+push 1
+#else
+push 2
+#end
+hlt
+    
+
+ +

+ When the assembler encounters an #if it generates a brz to the + corresponding #else or #end. Just like a conditional in a + high-level language, #if constructs can have #else branches + (which are executed if the top-of-stack is false) and can be nested inside one another. There is also an + #unless macro that acts the same as #if but inverted: it only + executes its body if the top-of-stack is 0: +

+ +
+
+.org 0x400
+push 0
+#unless
+push 1
+#else
+push 2
+#end
+    
+
+ +

+ The other macro construct is #while, which is used to create loops: +

+ +
+
+.org 0x400
+#while
+loadw counter ; Load the loop counter…
+lt 101 ; and check if it’s still 0..100
+#do
+loadw counter ; Increment the sum by the counter
+loadw sum
+add
+storew sum
+loadw counter ; Increment the counter by 1
+add 1
+storew counter
+#end
+loadw sum ; Finally load the sum and stop
+hlt
+counter: .db 0
+sum: .db 0
+    
+
+ +

+ The loop is divided into two sections: one condition between the #while and + #do, and a body between #do and + #end. The condition is executed first, and only if it leaves a nonzero on the stack (which + is consumed) is the body executed. Then the condition is executed again, and so on. Just like + #if, these can be nested (and nested inside conditionals and conditionals inside them), + and there's an inverted version called #until: +

+ +
+
+.org 0x400
+#until
+loadw counter ; Load the loop counter…
+gt 100 ; and check if it’s still 0..100
+#do
+loadw counter ; Increment the sum by the counter
+loadw sum
+add
+storew sum
+loadw counter ; Increment the counter by 1
+add 1
+storew counter
+#end
+loadw sum ; Finally load the sum and stop
+hlt
+counter: .db 0
+sum: .db 0
+    
+
+
+ + \ No newline at end of file diff --git a/vweb/src/snippet_emulator.css b/vweb/src/snippet_emulator.css index e35ac33..f13bc2a 100644 --- a/vweb/src/snippet_emulator.css +++ b/vweb/src/snippet_emulator.css @@ -21,8 +21,8 @@ body { .snippetEmulator { display: grid; - grid-template-rows: 10em 1em 1em 2em; - grid-template-columns: 20em; + grid-template-rows: auto 1em 1em 2em; + grid-template-columns: 40em; background: #333333; font-family: monospace; padding: 0.5em; @@ -59,4 +59,15 @@ body { .snippetEmulator .src .number { color: lightskyblue } .snippetEmulator .src .string { color: darkseagreen } .snippetEmulator .src .label { color: chocolate } -.snippetEmulator .src .comment { color: grey } \ No newline at end of file +.snippetEmulator .src .comment { color: grey } + +span.mono { + font-family: monospace; + font-weight: bolder; + color: white; +} +p.sidebar { + background-color: #555555; + border: 1px solid #aaaaaa; + padding: 0.5em; +} \ No newline at end of file