-
Notifications
You must be signed in to change notification settings - Fork 1
Data Model
This document details how different types of datas and objects are represented in WendyVM memory.
Primitives such as Strings, Numbers, Booleans, none are stored as a single data entry in the memory.
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.
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
...
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)
...
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.