Stackless Is Here, Today, Now! 🥞

I thought it would be informative to do a quick comparison of code, to see how something stackless is implemented...and how "messy" (or not) it makes things that were "formerly simple".


R3-ALPHA while loop

/***********************************************************************
**
*/	REBNATIVE(while)
/*
***********************************************************************/
{
	REBSER *b1 = VAL_SERIES(D_ARG(1));
	REBCNT i1  = VAL_INDEX(D_ARG(1));
	REBSER *b2 = VAL_SERIES(D_ARG(2));
	REBCNT i2  = VAL_INDEX(D_ARG(2));

	SET_NONE(D_RET);

	do {
		ds = Do_Blk(b1, i1);
		if (IS_UNSET(ds) || IS_ERROR(ds)) {	// Unset, break, throw, error.
			if (Check_Error(ds) >= 0) return R_TOS1;
		}
		if (!IS_TRUE(ds)) return R_RET;
		ds = Do_Blk(b2, i2);
		*DS_RETURN = *ds;	// save here (to avoid GC during error handling)
		if (THROWN(ds)) {	// Break, throw, continue, error.
			if (Check_Error(ds) >= 0) return R_TOS1;
			*DS_RETURN = *ds; // Check_Error modified it
		}
	} while (TRUE);
}

RED while loop

Red is different because it has an interpreted form and a compiled form of WHILE. This is just the interpreted form, which makes more sense to compare.

UPDATE July 2022: Red is dropping the compiled form due to the predictable difficulty of maintaining semantic parity of two different implementations. So this is the only WHILE to consider now.

while*:    func [
    check? [logic!]
    /local
        cond  [red-block!]
        body  [red-block!]
][
    #typecheck while
    cond: as red-block! stack/arguments
    body: as red-block! stack/arguments + 1
    
    stack/mark-loop words/_body
    while [
        assert system/thrown = 0
        catch RED_THROWN_BREAK [interpreter/eval cond yes]
        switch system/thrown [
            RED_THROWN_BREAK
            RED_THROWN_CONTINUE    [
                system/thrown: 0
                fire [TO_ERROR(throw while-cond)]
            ]
            0                     [0]
            default                [re-throw]
        ]
        logic/true?
    ][
        stack/reset
        assert system/thrown = 0
        catch RED_THROWN_BREAK [interpreter/eval body yes]
        switch system/thrown [
            RED_THROWN_BREAK    [system/thrown: 0 break]
            RED_THROWN_CONTINUE    [system/thrown: 0 continue]
            0                     [0]
            default                [re-throw]
        ]
    ]
    stack/unwind
    stack/reset
    unset/push-last
]

while: make native! [[
		"Evaluates body as long as condition block evaluates to truthy value"
		cond [block!]	"Condition block to evaluate on each iteration"
		body [block!]	"Block to evaluate on each iteration"
	]
	#get-definition NAT_WHILE
]

REN-C while loop

Terminology note: every frame has an extra scratch cell which is GC protected. This is called the "spare" cell. It's a good place to put temporary evaluations. For instance, the WHILE evaluates its condition there to avoid overwriting the output cell--since the result in OUT is supposed to be the previous body evaluation (or VOID if never written).

Implementation note: the comments are scraped by the build process and are the actual spec used for the function, so it can be maintained next to the source that uses it.

//
//  while: native [
//
//  "So long as a condition is truthy, evaluate the body"
//
//      return: "VOID if body never run, NULL if BREAK, else last body result"
//          [any-value?]
//      condition [<const> block!]
//      body [<unrun> <const> block! frame!]
//  ]
//
DECLARE_NATIVE(while) {
{
    INCLUDE_PARAMS_OF_WHILE;

    Value* condition = ARG(condition);
    Value* body = ARG(body);

    enum {
        ST_WHILE_INITIAL_ENTRY = STATE_0,
        ST_WHILE_EVALUATING_CONDITION,
        ST_WHILE_EVALUATING_BODY
    };

    switch (STATE) {
      case ST_WHILE_INITIAL_ENTRY : goto initial_entry;
      case ST_WHILE_EVALUATING_CONDITION : goto condition_was_evaluated;
      case ST_WHILE_EVALUATING_BODY : goto body_was_evaluated;
      default: assert(false);
    }

  initial_entry: {  //////////////////////////////////////////////////////////

    Add_Definitional_Break_Continue(body, LEVEL);  // don't bind condition [2]

} evaluate_condition: {  /////////////////////////////////////////////////////

    STATE = ST_WHILE_EVALUATING_CONDITION;
    return CONTINUE(SPARE, condition);

} condition_was_evaluated: {  ////////////////////////////////////////////////

    if (Is_Inhibitor(SPARE)) {  // null condition => return last body result
        if (Is_Fresh(OUT))
            return VOID;  // body never ran, so no result to return!

        return BRANCHED(OUT);  // put void and null in packs [3]
    }

    STATE = ST_WHILE_EVALUATING_BODY;  // body result => OUT
    return CATCH_CONTINUE_BRANCH(OUT, body, SPARE);  // catch break/continue

} body_was_evaluated: {  /////////////////////////////////////////////////////

    if (THROWING) {
        bool breaking;
        if (not Try_Catch_Break_Or_Continue(OUT, LEVEL, &breaking))
            return THROWN;

        if (breaking)
            return nullptr;
    }

    goto evaluate_condition;
}}

Here are the footnotes:

  1. It was considered if while true [...] should infinite loop, and then while false [...] never ran. However, that could lead to accidents of like while x > 10 [...] instead of while [x > 10] [...]. It is probably safer to require a BLOCK! vs. falling back on such behaviors. (It's now easy for people to make their own weird polymorphic loops.)

  2. We could make it so CONTINUE in the condition of a WHILE meant you skip the execution of the body of that loop, and run the condition again. That might be interesting for some strange stylized usage that puts complex branching code in a condition. But it adds some cost, and would override the default meaning of CONTINUE (of continuing some enclosing loop)...which is free, and enables other strange stylized usages.

  3. If someone writes:

    flag: okay
    while [flag] [flag: null, null]
    

    We don't want the overall WHILE result to evaluate to NULL--because NULL is reserved for signaling BREAK. Similarly, VOID results are reserved for when the body never runs. BRANCHED() encloses these states in single-element packs.

I Think The Work Speaks for Itself :speak_no_evil:

...but still, I'll say that not only is the Ren-C version expressed more clearly and documented more clearly, it is much more powerful.

And it all still compiles with TinyC. :star2:

4 Likes