Skip to main content

Gno Memory Model

The Typed Value

type TypedValue struct {
T Type
V Value
N [8]byte
}

Both Type and Value are Go interface values. Go does not support union types, so primitive values like bools and ints are stored in the N field for performance.

All values in Gno are stored represented as (type, value) tuples. Vars in scope blocks, fields in structs, elements in arrays, keys and values of maps are all respresented by the same TypedValue struct.

This tuple representation lets the Gno VM implementation logic be simpler with less code. Reading and writing values are the same whether the static type of the value is an interface or something concrete with no special logic for memory optimizations which is less relevant in a massive multi-user transactional operating system where most of the data resides in disk anyways.

Another benefit is that it promotes the development of new types of client interfaces that can make use of the type information for object display and interaction. The original vision of Tim Burners Lee's HTML DOM based World Wide Web with restful HTTP requests like GET and POST has been steamrolled over by continuous developments in HTML, CSS, Javascript, and the browser. The internet is alive but the World Wide Web is dead. Gno is a reboot of the original vision of the Web but better because everything is integrated based on a singular well designed object-oriented language. Instead of POST requests Gno has typed method calls. All values are annotated with their types making the environment REST-ful to the core.

REST (Representational State Transfer) is a software architectural style that was created to describe the design and guide the development of the architecture for the World Wide Web. REST defines a set of constraints for how the architecture of a distributed, Internet-scale hypermedia system, such as the Web, should behave. - wikipedia

While the Gno VM implements the memory model described here in the most straightforward way, future alternative implementations may represent values differently in machine memory even while conforming to the spec as implemented by the Gno VM.

Objects and Values

There are many types of Values, and some of these value types are also Objects. The types below are all values and those that are bolded are also objects.

  • Primitive // bool, uint, int, uint8, ... int64
  • StringValue
  • BigintValue // only used for constant expressions
  • BigdecValue // only used for constant expressions
  • DataByteValue // invisible type for byte array optimization
  • PointerValue // base is always an object
  • ArrayValue
  • SliceValue
  • StructValue
  • FuncValue
  • MapValue
  • BoundMethodValue // func & receiver
  • TypeValue
  • PackageValue
  • BlockValue // for package, file, if, range, switch, func
  • RefValue // reference to an object stored in disk
  • HeapItemValue // invisible type for loopvars and closure captures

Pointers

type PointerValue struct {
TV *TypedValue // escape val if pointer to var.
Base Value // array/struct/block, or heapitem.
Index int // list/fields/values index, or -1 or -2 (see below).
}

The pointer is a reference to a typed value slot in an array, struct, block, or heap item. It is also used internally in the VM for assigning to slots. Even internally the Base is used to tell the realm finalizer when the base has been updated.

Blocks and Heap Items

All statements enclosed in parentheses will allocate a new block and push it onto the block stack of the VM. The size of the block is determined by the number of variables declared in the block statement.

type BlockValue struct {
ObjectInfo
Source BlockNode
Values []TypedValue
Parent Value // Parent block if any, or RefValue{} to one.
Blank TypedValue // Captures "_" underscore names.
bodyStmt bodyStmt // Holds a pointer to the current statement.
}

The following Gno AST nodes when executed will create a new block:

  • FuncLitStmt
  • BlockStmt // a list of statements wrapped in
  • ForStmt
  • IfCaseStmt
  • RangeStmt
  • SwitchCaseStmt
  • FuncDecl
  • FileNode
  • PackageNode

IfStmts and SwitchStmts also produce faux blocks that get merged onto the following IfCaseStmt and SwitchCaseStmt respectively, but this is an invisible implementation detail and the behavior may change.

Heap items are only used in blocks. Conceptually they are an object container around a singleton typed value slot. It is not visible to the gno developer but it is important to understand how they work when inspecting the block space.

func Example(arg int) (res *int) {
var x int = arg + 1
return &x
}

The above code when executed will first produce the following block:

BlockValue{
...
Source: <*FuncDecl node>,
Values: [
{T: nil, V: nil}, // 'arg' parameter
{T: nil, V: nil}, // 'res' result
{T: HeapItemType{},
V: &HeapItemValue{{T: nil, V: nil}}, // 'x' variable
],
...
}

In the above example the third slot for x is not initialized to the zero value of a typed value slot, but rather it is prefilled with a heap item.

Variables declared in a closure or passed by reference are first discovered and marked as such from the preprocessor, and NewBlock() will prepopulate these slots with *HeapItemValues. When a *HeapItemValue is present in a block slot it is not written over but instead the value is written into the heap item's slot.

When the example code executes return &x instead of returning a PointerValue with .Base set to the BlockValue and .Index of 2, it sets .Base to the *HeapItemValue with .Index of 0 since a heap item only contains one slot. The pointer's .TV is set to the single slot of of the heap item. This way the when the pointer is used later in another transaction there is no need to load the whole original block value, but rather a single heap item object. If Example() returned only x rather than a pointer &x it would not be initialized with a heap item for the slot.

func Example2(arg int) (res func()) {
var x int = arg + 1
return func() {
println(x)
}
}

The above example illustrates another use for heap items. Here we don't reference x, but it is captured by the anonymous function literal (closure). At runtime the closure *FuncValue captures the heap item object such that the closure does not depend on the block at all.

Variables declared at the package (global) level may also be referred to by pointer in anonymous functions. In the future we will allow limited upgrading features for mutable realm packages (e.g. the ability to add new functions or replace or "swizzle" existing ones), so all package level declared variables are wrapped in heap item objects.

Since all global package values referenced closures can be captured as heap objects, the execution and persistence of a closure function value does not depend on any parent blocks. (Omitted here is how references to package level declared functions and methods are replaced by a selector expression on the package itself; otherwise closures would still in general depend on their parent blocks).

Loopvars

Go1.22 introduced loopvars to reduce programmer errors. Gno uses heap items to implement loopvars.

for _, v := range values {
saveClosure(func() {
fmt.Println(v)
})
}

The Gno VM does something special for loopvars. Instead of assigning the new value v to the same slot, or even the same heap item object's slot, it replaces the existing heap item object with a new one. This allows the closure to capture a new heap item object with every iteration. This is called a heap definition.

The behavior is applied for implicit loops with goto statements. The preprocessor first identifies all such variable definitions whether explicit in range statements or implicit via goto statements that are captured by closures or passed by pointer reference, and directs the VM to execute the define statement by replacing the existing heap item object with a new one.