.. figure:: logo.png :align: center http://www.bardolph.org .. index:: single: array .. _array: ****** Arrays ****** An array is created with the ``array`` keyword. After it has been declared, the elements can be accessed with numerical subscripts, which are contained in pairs of parentheses. The language allows for arrays to have an arbitrary number of dimensions. The size and dimension of an array are static and set when the array is declared. For example, to create a one-dimensional array: .. code-block:: lightbulb array vector(5) In this example, the array named `vector` has 5 elements and can be accessed with subscripts from 0 to 4. After an array has been created, it can be written with ``assign`` and read wherever any other variable is allowed. .. code-block:: lightbulb array hues(10) assign hues(5) 120 hue hues(5) Accessing an array with a subscript that is negative or outside the bounds of its size will cause an error to be logged, but in most cases, the virtual machine will make an attempt to provide a reasonable default. If the subscript is a floating-point value, the fraction will be truncated to yield an integer. To declare a multi-dimensional array, use a series of square brackets, each with a size. For example: .. code-block:: lightbulb array colors(8)(4)(9) assign colors(1)(3)(8) 100 Within the declaration, the size of the array can be calculated at run-tme. However, after an arrary has been declared, its size may not be changed. .. code-block:: lightbulb array hue_segment(hue / 10) assign array_size 24 * 60 array day(array_size) Assigning an array to a variable creates an alias and does not make a copy: .. code-block:: lightbulb array mat(3) assign mat(0) 100 assign mat2 mat assign mat2(0) 200 # This will print 200 print mat(0) .. index:: array; partial dereference Partially Dereferencing an Array ================================ When an array is partially subscripted, the result is itself an array with a reduced dimension, which can be kept in a variable. .. code-block:: lightbulb array mat_3(5)(5)(5) assign mat_2 mat_3(4) assign mat_2(0)(0) 100 assign vector mat_3(4)(2) assign vector(0) 200 .. index:: array; iterating over Iterating Over an Array ======================= To iterate over a specific range, use the ``repeat`` ``with`` construct: .. code-block:: lightbulb array vector(5) repeat with i from 1 to 3 assign vector(i) i * 2 To iterate over all of the elements in an array, use a ``repaeat`` ``in`` ``as`` clause: .. code-block:: lightbulb array hues(3) repeat in hues as i begin assign hues(i) i * 120 end Similar constructs can be used for multi-dimensional arrays: .. code-block:: lightbulb array colors(5)(4) repeat in colors as color_num repeat in colors(color_num) as setting_num assign colors(color_num)(setting_num) 100 repeat in colors(0) as setting_num assign colors(0)(setting_num) 200 array matrix(6)(5)(3) repeat in matrix as i repeat in matrix(i) as j repeat in matrix(i)(j) as k assign matrix(i)(j)(k) 500 .. index:: array; as a parameter Arrays as Parameters ==================== When an array is passed into a routine, it needs to be declared as such in the routine's definition. This is done with a pair of square brackets for each dimension. For example: .. code-block:: lightbulb define sum_array with arr()() begin assign sum 0 repeat with i from 0 to 4 begin repeat with j from 0 to 9 begin assign sum sum + arr(i)(j) end end return sum end array matrix(5)(10) # ...some code here fills the "matrix" array with data. println (sum_array matrix) (Maybe not in the first release) The code using the array can also be written without explicitly giving the lengths of each dimension: .. code-block:: lightbulb define sum_array with arr()() begin assign sum 0 repeat in arr as i begin repeat in arr(i) as j begin assign sum sum + arr(i)(j) end end end Arrays as Return Values ======================= A function is definition has no concept of a return type. In fact, nothing in the language prevents a function from potentially returning different types of values. Therefore, it will be up to the script writer to correctly use the return value from a function call. The parser will know if an incoming parameter, or a local variable, is an array, and handle it accordingly. However, it will not be able to do any compile-time checks on the return value of a function, because that is always determined at run-time. When an array has been fully dereferenced within a function before being returned, the return value is an immutable scalar. If the array is not fully dereferenced before being returned, the return value can be further indexed, potentially involving an assignment to one of its elements. Starting with a very simple example: .. code-block:: lightbulb array arr(10) assign arr(5) 2 define access_arr with vec() begin return vec(5) end As illustrated by this example: * The declaration specifies that ``vec`` is a one-dimensional array, but its size is not given. The script writer is responsible for avoiding an out-of-bounds index. * Because ``vec`` is fully dereferenced, the function returns 2. An example returning a locally-declared array: .. code-block:: lightbulb define return_arr begin array local_array(5) repeat in local_array as i begin assign local_array(i) i * 10 end return local_array end assign external_ref [return_arr] repeat in external_ref as j begin printf "{} " external_ref(j) end This script will print out: .. code-block:: 0 10 20 30 40 This example illustrates returning a partially-dereferenced array: .. code-block:: lightbulb define return_partial with arr()() n begin return arr(n) end array mat(3)(3) repeat in mat as i begin repeat in mat(i) as j begin assign mat(i)(j) i * 100 + j end end assign vec [return_partial 2] repeat with i in vec begin printf "{} " vec(i) end This script will output: .. code-block:: 200 201 202 This example has a function returning a partially dereferenced array that is then indexed: .. code-block:: lightbulb define return_partial with arr()() n begin return arr(n) end array vec(3)(3) assign [return_partial vec 1](2) 1000 println vec(1)(2) This outputs: .. code:block:: 1000 Design Overview =============== Support for arrays is accomplished with some new instructions and new handling of certain types of parameters passed into to stack-related instructions. An `Array` class contains a Python `List` object that holds the data. The class also keeps the declared size of the array. The `ArrayCursor` class is used to navigate arrays. It contains a reference to the underlying `Array` object, as well as an index value that designates a specific element within the `Array`. A specialized version of `ArrayCursor` is used to create arrays, as opposed to navigating them. New VM Instructions ------------------- OpCode.OP, Operator.SET ^^^^^^^^^^^^^^^^^^^^^^^ This is a new binary operation for the `vm_math` module. It does the following: #. Pop the rvalue from the top of the stack. #. Pop the lvalue from the top of the stack. #. Assign the rvalue to the lvalue. (Consider pushing the lvalue back onto the stack.) This code sequence does not leave anything on top of the stack. It replaces the previous implementation of the ``assign`` command. The implementation of the SET operation will need to handle array cursors differently than simple variables. Array-Centric Op-Codes ^^^^^^^^^^^^^^^^^^^^^^^ **OpCode.ARRAY**: Pop the name off the stack and create an array with that name. Push that array object back onto the stack. **OpCode.DEREF**: Pop the name of the array off the top of the stack. Create an array cursor using that name and push it onto the stack. **OpCode.INDEX**: Pop the index value off of the stack, and then pop the array object off of the stack. If the popped array object is a cursor, apply the index to the cursor and push the resulting array cursor back onto the stack. If the popped object is an array and not a cursor, add a dimension to the array with a length equal to the index value, and push the object back onto the stack. When the parser reaches the end of the square-bracket pairs, it adds an instruction to pop the stack to clear it of the array under construction. For example, this script: .. code-block:: lightbulb array a(10) can be accomplished with: .. code-block:: python OpCode.PUSHQ, "a" OpCode.ARRAY OpCode.PUSHQ, 10 OpCode.INDEX OpCode.POP In this case, `OpCode.INDEX` pops the array off the stack, pops 5 off the stack, and adds the dimension to the array, with 5 slots. The final pop instruction leaves the stack in its previous state. Then, to reference the array, such as `a(5)`: .. code-block:: python OpCode.PUSHQ, "a" OpCode.DEREF OpCode.PUSHQ, 5 OpCode.INDEX After this code has been executed, the array cursor will occupy the top of the stack, with the index value of 5. If it is used as an lvalue, that cursor stores the incoming value in the underlying array, at position 5. If it is used as an rvalue, it provides the value stored in the underlying array, also at position 5. Therefore, this: .. code-block:: lightbulb hue a(5) would produce something like: .. code-block:: python OpCode.PUSHQ, "a" OpCode.DEREF OpCode.PUSHQ, 5 OpCode.INDEX OpCode.POP, Register.HUE Note that the stack returns to the same condition it was in prior to this code being executed. Conversely, assignment to the array, such as: .. code-block:: python assign a(5) 10 can be done with: .. code-block:: python OpCode.PUSHQ, "a" OpCode.DEREF OpCode.PUSHQ, 5 OpCode.INDEX OpCode.PUSHQ, 10 OpCode.OP, Operator.SET In response to the SET command, the VM pops the array cursor, pops the name of the destination variable ("a"), and assigns the value from the cursor to the variable. The stack will not have anything added to it. Compare this to the code associated with a scalar: .. code-block:: python assign b 20 which might produce: .. code-block:: python OpCode.PUSHQ, "b" OpCode.PUSHQ, 20 OpCode.OP, Operator.SET In both cases (scalar or array), the `SET` operation leaves the stack in its previous state. Multi-Dimensional Arrays ^^^^^^^^^^^^^^^^^^^^^^^^ Two kinds of array cursors handle `OpCode.INDEX`. A cursor used for creation responds to indexing by adding another dimension to the array. For example, to have v(10)(5): .. code-block:: lightbulb array v(10)(5) this can be done with: .. code-block:: python OpCode.PUSHQ, "v" OpCode.ARRAY OpCode.PUSHQ, 10 OpCode.INDEX OpCode.PUSHQ, 5 OpCode.INDEX OpCode.POP Note that the first `OpCode.INDEX` creates the first dimension, creating the equivalent of `v(10)`. The second `OpCode.INDEX` creates the second dimension. In the underlying array object, each of the first 10 elements is populated with a 5-element sub-array, which amounts to `v(10)(5)`, for a total of 50 slots. In this context, every `INDEX` instruction leaves the array object on the stack. No cursors are created here. When construction of the array is complete, the `POP` at the end clears it off the stack. Assignment uses an array cursor that acts as a gateway to the underlying array. For example: .. code-block:: lightbulb assign x v(2)(3) can be done with: .. code-block:: python OpCode.PUSHQ, "x" OpCode.PUSHQ, "v" OpCode.DEREF OpCode.PUSHQ, 2 OpCode.INDEX OpCode.PUSHQ, 3 OpCode.INDEX OpCode.SET Conversely, to assign in the opposite direction: .. code-block:: lightbulb assign x v(4)(1) can be done with: .. code-block:: python OpCode.PUSHQ, "v" OpCode.PUSHQ, 4 OpCode.INDEX OpCode.PUSHQ, 1 OpCode.INDEX OpCode.PUSHQ, "x" OpCode.OP, Operator.SET Partially-Dereferenced Arrays ----------------------------- Only one cursor is needed when iterating over a one-dimensional array. Arrays of more than one dimension require one cursor for each dimension. This is true during creation and access. Returning to example: .. code-block:: lightbulb array v (5)(10) assign x v(3) assign y x(7) the cursors work as follows: .. code-block:: python # array v (5)(10) # OpCode.PUSHQ, "v" OpCode.ARRAY OpCode.PUSHQ, 2 OpCode.INDEX OpCode.PUSHQ, 3 OpCode.INDEX OpCode.POP # assign x v(3) # OpCode.PUSHQ, "x" OpCode.PUSHQ, "v" OpCode.DEREF OpCode.PUSHQ, 3 OpCode.INDEX OpCode.OP, Operator.SET # assign y x(7) # OpCode.PUSHQ, "y" OpCode.PUSHQ, "x" OpCode.DEREF OpCode.PUSHQ, 7 OpCode.INDEX OpCode.OP, Operator.SET Array as Parameter ------------------ The key concept here is that the incoming parameter is a cursor, and there is no DEREF instruction inside the routine. Script: .. code-block:: lightbulb define fn with v() begin assign v(7) 9 return v(5) end array arr(10) assign arr(5) 250 hue (fn arr) VM code: .. code-block:: python # define fn with v() begin # OpCode.ROUTINE, "fn" # assign v(7) 9 # OpCode.PUSHQ, "v" OpCode.PUSHQ, 7 OpCode.INDEX OpCode.PUSHQ, 9 OpCode.OP, Operator.SET # return v(5) # end # OpCode.PUSHQ, "v" OpCode.PUSHQ, 5 OpCode.INDEX OpCode.RETURN # Functions leave the return value on the stack. # array arr(10) # OpCode.PUSHQ, "arr" OpCode.ARRAY OpCode.PUSHQ, 10 OpCode.INDEX OpCode.POP # assign arr(5) 250 # OpCode.PUSHQ, "arr" OpCode.DEREF OpCode.PUSHQ, 5 OpCode.INDEX OpCode.PUSHQ, 250 OpCode.OP, Operator.SET # hue (fn arr) # OpCode.CTX OpCode.PUSHQ, "arr" OpCode.DEREF OpCode.POP, Register.RESULT # Register.RESULT now holds a cursor. OpCode.PARAM, "v", Register.RESULT # Pass that cursor as parameter v. OpCode.JSR, "fn" OpCode.END_CTX OpCode.POP, Register.RESULT OpCode.MOVE, Register.RESULT, Register.HUE Partially-Dereferenced Array as a Parameter ------------------------------------------- As with the previous use case, the incoming parameter is a cursor, and there is no DEREF instruction inside the routine. Script: .. code-block:: lightbulb define fn with v() begin assign v(6) 7 return v(5) end array arr(10)(20) assign arr(3)(5) 100 hue (fn arr(3)) VM code: .. code-block:: python # define fn with v() begin # OpCode.ROUTINE, "fn" # assign v(6) 7 # OpCode.PUSHQ, "v" OpCode.PUSHQ, 6 OpCode.INDEX OpCode.PUSHQ, 7 OpCode.OP, Operator.SET # return v(5) # end # OpCode.PUSHQ, "v" OpCode.PUSHQ, 5 OpCode.INDEX OpCode.RETURN # array arr(10)(20) # OpCode.PUSHQ, "arr" OpCode.ARRAY OpCode.PUSHQ, 10 OpCode.INDEX OpCode.PUSHQ, 20 OpCode.INDEX OpCode.POP # assign arr(3)(5) 100 # OpCode.PUSHQ, "arr" OpCode.DEREF OpCode.PUSHQ, 3 OpCode.INDEX OpCode.PUSHQ, 5 OpCode.INDEX OpCode.PUSHQ, 100 OpCode.OP, Operator.SET # hue (fn arr(3)) # OpCode.CTX OpCode.PUSHQ, "arr" OpCode.DEREF OpCode.PUSHQ, 3 OpCode.INDEX OpCode.POP, Register.RESULT # Cursor associated with arr(3). OpCode.PARAM, "v", Register.RESULT OpCode.JSR, "fn" OpCode.END_CTX OpCode.POP, Register.RESULT OpCode.MOVE, Register.RESULT, Register.HUE Loop over 1-D Array ------------------- This kind of loop resembles a standard counted loop. .. code-block:: lightbulb array v(10) repeat with i from 0 to 9 hue v(i) Note that the above code is functionally equivalent to: .. code-block:: lightbulb array v(10) repeat in v as i hue v(i) Therefore the parser, generates code similar to that of a counting loop. Because the size of the array is calculated at run-time, the limit for the loop must also be determined dynamically. The new `Operand.SIZE` designates this. For example, to retrieve the size of array `v` and leave it on top of the stack: .. code-block:: python OpCode.PUSHQ, "v" OpCode.OP, Operator.SIZE To integrate this into loop code: .. code-block:: python # Enter the loop. OpCode.LOOP # Initialize the counter # OpCode.PUSHQ, "v" OpCode.Op, Operator.SIZE OpCode.POP, LoopVar.COUNTER # This is where the JUMP code points. Check that the number of remaining # iterations > 0. # OpCode.PUSH, LoopVar.COUNTER OpCode.PUSHQ, 0 OpCode.OP, Operator.GT OpCode.JUMP, JumpCondition.IF_FALSE, 11 # (Code inside the loop goes here.) # Decrement the counter. # OpCode.PUSH, LoopVar.COUNTER OpCode.PUSHQ, 1 OpCode.OP, Operator.SUB OpCode.POP, LoopVar.COUNTER # Jump to the next potential iteration, where the counter is compared to 0. # OpCode.JUMP, JumpCondition.ALWAYS, -5 # Exit the loop. # OpCode.END_LOOP Interpolation works the same as with counted loops as well: .. code-block:: lightbulb array v(10) repeat in v as i with x from 10 to 100 hue v(i) + x Loop over 2-D Array ------------------- Script: .. code-block:: lightbulb array m(5)(10) repeat in m as i repeat in m(i) as j assign m(i)(j) 0 repeat in m as i repeat in m(i) as j hue m(i)(j)