Skip to content

Data Model

Felix Guo edited this page Mar 10, 2019 · 7 revisions

This document details how different types of datas and objects are represented in WendyVM memory.

Primitives

Primitives such as Strings, Numbers, Booleans, none are stored as a single data entry in the memory.

Range

A range object is also stored as a single data entry with the type RANGE. The bounds of the range object is actually stored in the string component of the data value, two numbers separated by a |, in this format: %d|%d. Methods to extract each component can be found in data.c.

List

A list object is a reference to a LIST_HEADER object, after which are the actual contents of the list. The list header stores the length of the list. Suppose we had this code:

let a = [10, 20, 30]

The call stack would have the mapping a -> 5, where the LIST object is stored at address 5. This is an example of how the memory could look:

ADDRESS     DATA_TYPE       VALUE
================================
...
5           LIST            8       <- points to LIST_HEADER
...         
8           LIST_HEADER     3       <- size of list
9           NUMBER          10
10          NUMBER          20
11          NUMBER          30
...

This means that retrieving the size of a list can be done in constant time, and lists are always passed by reference. A copy of a list can be made with the ~ operator. If we took the previous example:

let a = [10, 20, 30]
let b = a

The callstack would have the mapping a -> 5 and b -> 13, but both a and b would point to the same list:

ADDRESS     DATA_TYPE       VALUE
================================
...
5           LIST            8       <- points to LIST_HEADER
...         
8           LIST_HEADER     3       <- size of list
9           NUMBER          10
10          NUMBER          20
11          NUMBER          30
...
13          LIST            8       <- SAME ELEMENTS!
...

However, if we wrote let b = ~a instead, then:

ADDRESS     DATA_TYPE       VALUE
================================
...
5           LIST            8       <- points to LIST_HEADER
...         
8           LIST_HEADER     3       <- size of list
9           NUMBER          10
10          NUMBER          20
11          NUMBER          30
...
13          LIST            16      <- shallow copy of A, points to different list
...         
16          LIST_HEADER     3       <- size of list
17          NUMBER          10
18          NUMBER          20
19          NUMBER          30
...

Functions

A function object is a reference to an ADDRESS followed by a CLOSURE object. The ADDRESS stores the address to the first instruction to execute in the function, and the CLOSURE stores the index of the corresponding closure in the list of closures. Lambda functions are assigned a temporary name.

let makeadd => (adder) #:(addend) adder + addend
ADDRESS     DATA_TYPE       VALUE
================================
...
5           FUNCTION        8       <- makeadd function (points to ADDRESS)
...         
8           ADDRESS         20      <- address to first instruction
9           CLOSURE         0       <- first closure created (no local var)
...
20          FUNCTION        23      <- ~lambda1 function (points to ADDRESS)
...
23          ADDRESS         23      <- address to first instruction of lambda
24          CLOSURE         1       <- second closure created (has map of adder)
...

Structures

WendyScript uses a class based system for managing object oriented programming. A structure can be declared with an optional parent, instance members, and static members. Suppose we wrote:

struct position => (x, y) [print]

A structure prototype would be setup in memory that looks like this:

ADDRESS     DATA_TYPE       VALUE
===========================================
...
5           STRUCT_METADATA     8           <- size of metadata
6           STRUCT_NAME         "position"  <- debugging purposes
7           STRUCT_STATIC       "init"      <- init function
8           FUNCTION            10          <- static value of "init", defaults to auto-generated init function
9           STRUCT_PARAM        "x"         <- first parameter
10          STRUCT_PARAM        "y"         <- second parameter
11          STRUCT_STATIC       "print"     
12          NONE                            <- static value of "print", defaults to none
...
15          STRUCT              5           <- points to metadata

The call-stack would then hold a binding position -> 15. This means that classes can be passed to functions, and can be created in a function, then returned as a value. If I wrote:

struct position => (x, y) [print]
let other_position = position

I could create a position object with let a = other_position(10, 20), which is just as valid as let a = position(10, 20). Invoking a call expression on an identifier bound to a STRUCT will create a STRUCT_INSTANCE, bind it to this, then call the init function. By default, the init function is constructed based on the instance members, so in this case, the init function generated by default looks like this:

position.init => (x, y) {
    this.x = x
    this.y = y
    ret this
}

When a instance of a structure is created (building upon the values in memory listed before):

let a = position(10, 20)

A bare bones instance is created:

ADDRESS     DATA_TYPE               VALUE
========================================
...
20          STRUCT_INSTANCE_HEAD    4       <- size of instance
21          STRUCT_METADATA         5       <- points to prototype of structure
22          NONE                            <- refers to first instance member (x)
23          NONE                            <- second instance member (y)    
...
25          STRUCT_INSTANCE         20      <- points to instance
...

A reference to this bare bones instance is bound to this upon entering the init function. The default init is run above, assigning the corresponding values:

ADDRESS     DATA_TYPE               VALUE
========================================
...
20          STRUCT_INSTANCE_HEAD    3       <- size of instance
21          STRUCT_METADATA         5       <- points to prototype of structure
22          NUMBER                  10      <- refers to first instance member (x)
23          NUMBER                  20      <- second instance member (y)    
...
25          STRUCT_INSTANCE         20      <- points to instance
...

When the init function returns this, the call stack maps a -> 25. When a member is accessed, a.x, the metadata is traversed. WendyVM notes that x is the first instance member, so will take the first value encountered after STRUCT_METADATA in the STRUCT_INSTANCE. When a static member a.print is accessed, WendyVM takes the next value upon finding the corresponding field in the metadata, since all static values are stored right below the STRUCT_STATIC declaration in the metadata.

Clone this wiki locally