Operator parameter passing semantics

Operator parameters in SPL behave similarly to macro expansion, but the semantics are designed to prevent macro mistakes common in languages like C.

In particular, SPL defines a name resolution rule to prevent accidental name capture and a syntactic confinement rule to prevent unintended reassociation. This macro expansion approach for composite operator customization was chosen because it makes operators concise, provides flexible typing, and naturally generalizes to primitive operators. It also simplifies the implementation, since certain entities (operator, type, and function parameters) are fully resolved at compile time, and need not be treated as first-class values at run time.

The name resolution rule for names in actual parameters is that the SPL compiler resolves them in the context of the operator invocation, not in the context of the operator definition. The following example illustrates how SPL resolves names when there are multiple choices:

composite Comp1 {
  type  T = int32;
  graph () as Op = Comp2() { param U : T; }
}
composite Comp2 {
  param type $U;
  type  T = boolean;
  graph stream<int32 x> A = Beacon() { }
        stream<int32 x> B = Functor(A) {
         logic state : $U v = 0;
        }
}

The actual value for parameter U in Line 3 is T. The name is resolved in the context of the operator invocation in Line 3, where T is int32. If the name was resolved in the context of the operator definition, then, since $U is used in Line 10, T would be Boolean. By resolving T to Comp1.T, the language shields the author of Comp1 from implementation details of Comp2. The rule "resolve names in the invocation, not the definition" supports encapsulation, because it hides implementation details of the operator, rather than allowing them to leak out by capturing names.

Note: SPL operator parameters are "hygienic". If the expanded expression only uses build-time values, it is fully evaluated at build time. Alternatively, if the expanded expression also involves runtime values, only the build-time subexpressions are partially evaluated, and the rest is evaluated at run time, with pass-by-name semantics.

The syntactic confinement rule for SPL operator parameters is that argument expressions are implicitly parenthesized. The implicit parentheses "confine" them from associating in unexpected ways when they get used. A corollary of this rule is that you cannot pass something that does not start out confined; for example, an incomplete expression like x + is not valid as an actual parameter to an operator. The following example illustrates the syntactic confinement rule:

composite M(output Out; input In) {
  param expression<int32> $q;
  graph stream<float32 percent> Out = Functor(In) {
         output Out: percent = (float32)(100 * $q);
        }
}
composite Main {
  graph stream<int32 x> A = Beacon() {}
        stream<float32 percent> B = M(A) { param q: x - 1; }
}

The actual parameter x - 1 on Line 9 is substituted for the parameter $q on Line 4. Because of the syntactic confinement rule, 100 * $q associates like 100 * (x - 1), not like (100 * x) - 1. As with the name resolution rule, the syntactic confinement rule leads to better encapsulation when an operator is invoked. You need not worry about precedence context in the operator definition. This logic prevents unintended results.

Note: Parameter expressions yield a subtree in the AST and do not spill out of this subtree during expansion.