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:
- Main RunRecord: indicates PC (ProgramCounter) 52,
the instruction after FCal to
SFuncinPFunc. - Resume RunRecord: indicates PC 47, the instruction
immediately after
YieldinSFunc. (It is actually a one-byte No-op, followed by a Jmp!) - Stack Copy: contains 4 elements placed on the run-stack
by
MapIfor its continued operation. These have to be restored when jumping back in.
// 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 VOutOf 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.