Menu

scScript Map Iteration

scScript currently provides two primitives, MapI and MapK, to map (iterate) over arrays and dicts:
MapI(S)(V, I) Operation;
or MapI(S)(V, I) {Operations...}

MapI (Map Index) will perform Operation(s) for every single cell of S, which is usually an array or dict. If the out variables V and I are provided, they will get the value of individual array elt or dict pair and the 0-based index respectively. New arrays are generally created with missing elt, but MapI will still index every cell, it will execute Operation(s) and set V to Int 0 for empty cells. Similarly, MapI will index through the closed hash array for dicts, going down every individual cell, even the ones that do not indicate a pair. Since there is no rhyme nor reason to hash indices, a MapI traversal of a dict will not visit its pairs in any user-discernible order.
MapK(S)(V, K) Operation;
or MapK(S)(V, K) {Operations...}

MapK (Map Key) will perform similar to MapI, but will key only on non-empty cells in arrays and dicts. In the case of arrays, MapK executes Operation(s) and sets V and K for each elt. When it comes to dicts, MapK traverses the chain of pairs, ignoring the hash array completely. If provided, V will get the value and K the storage key for the pair. Interestingly, dicts are LIFO, so the last pair to be added will be traversed first with MapK. In general, while both MapI and MapK make sense for arrays, but only MapK is used for dicts.

Both MapI and MapK traverse other data, such as numbers or strings, in a single step. So the Operation(s) in the body of the Map will be performed only once and I or K will be set to Int 0.

MapI (and MapK) will place the value of the array elt (or dict pair) in the V variable. They will not set V (alias it) to have the same storage as the elt (or pair value). While aliasing was initially contemplated for MapI, it was soon overruled because the user cannot easily know if an elt is missing or really has an Int 0 value. If the cell was missing, a new temporary variable would have to be created to satisfy the alias, and (confusingly) assigning to it would not change the array! (Alternatively, all elts of an array could be populated when Mapping, but this would be inefficient; besides, dicts rely on empty cells for their hash storage algorithm.)

While Map iterators look like scScript function calls, they are internally implemented as IFIDs, sequences of bytecodes that are added inline with no function setup/call overhead. The bulk of the work is done by efficient low-level IMap and KMap bytecodes, forming loop centers.

Implementation Details

The following example shows the interaction between MapI and coroutines with non-local exits. PFunc defines an array, stashes its own RC, then Submits to its caller, the main script. The script will in turn Resume PFunc, which in turn calls SFunc, handing it the array and its RC. This subordinate function will perform the MapI, and Yield OutOf PFunc to the main script.

The complexity becomes obvious when the script shows the PRC contents, which now has 3 parts:

// MAP and CoRoutines!  (CRMap.sc)

Function SFunc(A, RC)()
{
    Var	V, I;
    
    // Will yield mid-MapI
    MapI(A)(V, I) {
	Writeln(I, " --> ", V);
	if (I == 1) Yield V OutOf RC;
    }
}

Function PFunc()()
{
    Var TheArr = ["First", "Second", "Third"];
    Var FRC = GetRC();

    Submit();
    SFunc(TheArr, FRC);
}

Var PRC, Res;

PRC = PFunc();	    // Gets RC from PFunc Submit!
Res = Resume(PRC);  // SFunc will Yield mid-MapI...
Show.Vars(Show.#Max)(Res, PRC);

Resume(PRC);	    // Will Return from PFunc and end.

The following is the *Output* of the above script:

0 --> First
1 --> Second
    Var (Ref:1) [Entry 000048 (16 Bytes) on Mem:1 (VEnt) Slab:1]
        --> Str (Ref:4) [Entry 020032 (32 Bytes) on Mem:2 (BEnt) Slab:1]
            --> 6 Bytes [Entry 003376 (24 Bytes) on Mem:3 (SDat) Slab:1]
                "Second"

    Var (Ref:1) [Entry 000032 (16 Bytes) on Mem:1 (VEnt) Slab:1]
        --> RC (Ref:5) [Entry 020416 (32 Bytes) on Mem:2 (BEnt) Slab:1]
            --> Run PC:52 [Entry 008336 (72 Bytes) on Mem:5 (Chnk) Slab:1]
                --> Code:2 (Ref:2) [Entry 000224 (104 Bytes) on Mem:5 (Chnk) Slab:2]
                --> 001 Loc Var (Ref:1) [Entry 000064 (16 Bytes) on Mem:1 (VEnt) Slab:1]
                --> 002 Loc Var (Ref:1) [Entry 000080 (16 Bytes) on Mem:1 (VEnt) Slab:1]
            Resume RunRecord
            --> Run PC:47 [Entry 008408 (88 Bytes) on Mem:5 (Chnk) Slab:1]
                --> Code:1 (Ref:2) [Entry 000120 (104 Bytes) on Mem:5 (Chnk) Slab:2]
                --> 001 In Var (Ref:1) [Entry 000160 (16 Bytes) on Mem:1 (VEnt) Slab:1]
                --> 002 In Var (Ref:1) [Entry 000176 (16 Bytes) on Mem:1 (VEnt) Slab:1]
                --> 003 Loc Var (Ref:2) [Entry 000192 (16 Bytes) on Mem:1 (VEnt) Slab:1]
                --> 004 Loc Var (Ref:2) [Entry 000208 (16 Bytes) on Mem:1 (VEnt) Slab:1]
            Stack Copy
            --> 4 Elts [Entry 004376 (80 Bytes) on Mem:4 (ADat) Slab:1]

2 --> Third

   *********************************************
   SCRIPT RETURN -->  0
   *********************************************

Interestingly, the FRC variable in PFunc and PRC returned by Submit are exactly the same RC, as every function has one and only one RC. But Resume RunRecord and Stack Copy would be NULL (and therefore not shown) if Show.Vars were to be called from PFunc, because it would be running and not (suspended) awaiting resumption.

When the second Resume is called in the main script, control flow returns to SFunc immediately after Yield. The third and final iteration of MapI would call Writeln and SFunc would terminate, returning to PFunc which would also return.

Normally, SFunc would be a tail call from PFunc, and scScript would optimize it so SFunc returns directly to the main script. But tail call optimization is turned off for coroutines and where GetRC() is called for non-local returns.