Action Walkthrough, part 2

There are two protocols for invoking Actions that are under consideration. Both are described here. The example code for the discussion was shown in the previous article.

Both protocols agree that the Actor invokes the Action directly through a method (rather than using the SendMessage(msg,addr) run-time API. This means that the Action meta-class must have an invoke(self,msg) method.

[There is still some question about direct linkage versus API linkage. I’m leaning towards direct at the moment.]

Another common characteristic is that most Actions are single-clause, so most clause parameters lists have only one item (the parameters for the single clause). The behavior is identical between single-clause Actions and the first clause of multi-clause Actions. The difference is that the latter go on to process the additional clauses.

The items of the clause parameters list are clause objects, which are lists where the first item is the clause name and the remainder comprise an ordered list of clause parameters (always the address of some object).

The two protocols differ in how they receive inputs. In the first case, the Actor passes itself to its Action. In the second case, the Actor pushes a parameter list.

Action receives passed Actor or (Actor’s) clause parameters list:

An Action’s invoke() method expects a list containing zero or more clause objects. The list is passed directly from the caller (usually the Actor’s dispatch() method). This is analogous to how an Instance’s dispatch() method passes the Instance directly to the corresponding Model’s handle() method.

There is the question of whether to pass the entire Actor object (using a BOOL address) or to pass an internal list of clause objects (taken directly from the Actor object). Because invoking the Action is internal, there is no requirement to pass a BOOL address.

On the other hand, passing the entire Actor object gives access to the address of the temporary result object, which means the Action could populate it. No, that requires write access to the calling Action’s Call Frame, and I’m not sure that’s supported.

Action receives clause parameters on the stack:

An Action’s invoke() method takes no parameters, but expects a list of zero or more clause objects on the stack. The list is created and pushed by the invoking party.

The second protocol matches the most natural way of returning outputs — by pushing them to the stack. OTOH: it’s ugly. Either you push a (potentially not small) list of clause parameters, or you need more complicated logic to process clause objects individually off the stack. (The problem is lack of look-ahead.) Either way there is a clumsiness, plus pushing inputs defies the idea of direct linkage from Actor to Action, which is meant to echo the one from Instance to Model.

The bottom line seems to be that either Action inputs and outputs use different protocols, or Actions find a way to return outputs directly, because passing a list of clause objects seems the most preferred.

Recall that our Action has a clause list that looks like this:

[ ('hypot', 2, execute-list) ]

Which specifies a single-clause (named “hypot”) that takes two parameters. The clause object that satisfies this clause is:

('hypot', address, address)

So to invoke the clause, the Action meta-code copies the two parameters to the appropriate slots in the Call Frame and sends a Q: Message to the execute-list. That process occurs for all Actions (single- or multi-clause), and applies generally to each additional clause — copy parameters to the Call Frame, invoke the execute list. Repeatable clauses just loop back to re-do the clause.

The general algorithm looks like this:

for obj in action.initlist:
    send-message("X:", obj)
curr-param = inputs.first-param()
for clause in action.clauses:
    if curr-param is None:
        break  // no more input objects
    if curr-param.name != clause.name:
        copy-params-to-call-frame()
        send-message("X:", clause.execlist)
        curr-param = inputs.next-param()
for obj in action.exitlist:
    send-message("Q:", obj)

The single-clause case is pretty simple. Note that, if the inputs were coming from the stack individually, the final next-param() call would return a non-clause object it shouldn’t (or worse, hit the stack bottom). The code assumes the input list can be iterated.

Try the logic on a longer multi-clause case with repeating clauses: the @if Action, which has @elif and @else clauses. Both secondary clauses are optional; the @elif clause is repeatable. The @if Action’s clause list looks like this:

[ ('if', 2, if-exec-list)
, ('elif', 2, elif-exec-list)
, ('else', 1, else-exec-list) ]

And a putative input clause parameters list might look like this:

[ ('if',addr,addr)
, ('elif',addr,addr), ('elif',addr,addr), ('elif',addr,addr)
, ('else',addr) ]

Which represents an if-elseif-else statement with three elseif parts. (Assume that each addr above is a distinct address.)

The Action’s @elif clause is repeated for each of the input clauses. Other possible input lists might look like this:

[ ('if',addr,addr), ('else',addr) ]

Which represents a standard if-else statement with no else-if clauses. Since no clause objects are provided as inputs, that clause is skipped.

An even simpler version is:

[ ('if',addr,addr) ]

Which represents a simple if statement with no other parts. Both secondary clauses are skipped in this case, and the primary if clause executes on its own.

Advertisements
%d bloggers like this: