How Final Works
Final is written in Go, and takes full advantage of all the features Go offers:
- fully self-contained binaries
- Garbage Collected memory management
- Memory allocator optimized for modern workloads
- rich set of building blocks, such as Utf-8 strings, growable slices (arrays), maps
- generics, to simplify creating a template class and typed instantiations
- excellent threading model in goroutines and channels
- simple project setup, and inclusion of packages
- fast build times
Final itself was designed to be highly efficient at runtime, both in terms of memory consumption, as well as execution speed.
Final Virtual Machine (VM)
Final code executes in a Virtual Machine. Final code is a combination of Axioms, which are written in Go, Go-native Type member accessors written in Go, and Final Colon Words.
An Axiom is a fundamental operation, such as + dup and other such low-level operations that provide the core “machine code” of Final.
OpCodes and the Code Table
Each Final Axiom has an integer OpCode, that gets compiled into something called the Code Table, which is a Go slice of ints. The VM has an Instruction Pointer (IP) that points to the next cell in the CT. The basic operation of the VM is to read the next OpCode, then dispatch to its Go code, using an efficient Go switch statement that is compiled at the machine code level to a fast, efficient jump table. Due to how modern processors cache often-used instructions and data, the Final VM is blazing fast, and overall, often comparable to compiled machine code as generated by languages like Go. Because the actual machine code making up Final programs is more limited than large programs in compiled languages, most of its actual machine code typically stays “hot” in the L0 or L1 cache of the CPU, resulting in excellent performance. So don’t imagine that because Final compiles to a VM, that its code will be substantially slower than native code!
The other place Go-native code appears, is in so-called native members of most of Final’s runtime types. But again there, it overall represents a relatively small amount of compiled machine code, and also benefits from effective hot caching.
The Code Table holds the following types of entries:
- Axiom OpCodes
- so-called “arming” prefixes, that provide a needed subcode, context selector, or Data Table index (see below) to the following OpCode
- literal Bool, Int, Uint or Float values, read by a DoLiteral opcode, that reads the immediate value, and pushes the literal on DS
- Nest, followed by the address of a compiled colon word; this pushes the current IP on the Return Stack (RS), and dispatches the VM to the code of the colon word
- Unnest, which is compiled by
;that returns to the code after the colon jump
The Data Table
To keep execution efficient, the Code Table is limited to a single int value in each slot. When a literal that takes more space needs to be compiled for a word, such as a String literal, the word allocates a slot in the Data Table, which is a go slice of ‘any’ values, which are 128 bits long. Data Table slots are also used to hold constants and variables defined in Final source, and for some other specialized purposes.
Dictionary Context
The trio of a Dictionary, Code Table, and Data Table make up what is called a Dictionary Context (DC). A compiled colon word has all its parts colocated in a single DC. During bootstrap and compiling of core Final words, this DC is the global core DC. When you compile your words, you are compiling into your Session-local DC. Colon words in one DC can seamlessly include words in another DC; the code needed to jump and return to that DC’s Code Table is compiled automatically into the Nest operation.
Stacks
All of Final’s stacks are instances of a Go Stack class, which is built on a Go slice of any. The stacks are checked at runtime for underflow, and overflow, to limit the effects of runaway code or unchecked recursion. The Data Stack is given a small initial capacity value, and the auxilliary stacks none. All stacks automatically expand as needed, up to their limit.
Tasks and Sessions
From a user perspective, Final code executes under the auspices of a Task, in something called a Session. The Task hold static information about the runtime context, such as its Dictionary Context. The runtime is fully multi-threaded, and supports any number of Task/Session pairs.
A Session provides the API for communicating with the the instance of the VM defined by the task. It includes a Send function that receives text, provides it as input to the running instance, and waits for a reply. This is the basis for the final Prompt/Reply sequence, and the embedded instance Send command, for example an AI Tool prompt, that can include word definitions and immediate commands.
Error Handling
Each running instance of the Final VM captures runtime errors, and resets the VM to a known good state. It reports the fault, resets the stacks, and does a “warm boot” of the VM, which puts in back into interpret mode. Final also includes Structure Exception Handling, described in the section Error and Exception Handling .
Threading
Pips are based on goroutines, so enjoy the benefits of an M:N dispatch model that limits the number of actual OS threads to what the CPU can provide via cores, then using an efficient dispatch routine to assign ready-to-run goroutines/Pips to an available core.
Pipes are based on go channels, and likewise enjoy all their benefits, such as being threadsafe, efficient, and (fairly) easy to reason about.
Each Pip in a Session gets a clone of its parent Task, and shares the Dictionary Context with it. Each Pip gets its own set of stacks, and executes in its own instance of the VM run loop. When the spawn word of a Pip finishes, it returns to an Exit opcode, which causes the run loop to stop, and the underlying goroutine to finish. It reports that it is done via a control channel, to its parent. The DC of the root Task remains locked as long as any child Pips exist. Once all end, the DC unlocks. It would not be unusual for a Final program to compile all its words, create any global variable or constants need, and then spawn its workers, rendering the DC locked for the remainder of the runtime.
Because spawn: requires a definite Final colon word to be dispatched, Pips do not have an interpreter, they simply execute the code of the spawn word.
By default, spawned words inherit the stdin/out of the parent. The stdio streams are threadsafe, and can thus demultiplex the output from child Pips, if writing to stdout is a component of the design.
Files and I/O
Final’s Stream classes are based on a 2-level division of surface and provider. The surface level provides the APIs shared by all particular types, such as ReadByte or WriteLine; provider objects for each type of Stream, such as file, stdio, string, etc. are plugged in to the surface object, and the surface object simply delegates to the provider. This design provides good performance, simplicity, and extensibility, for example to compose streams that have intermediaries that do things like compress, buffer, or encrypt/decrypt.