Skip to content

Returning multiple values: semantics

This is an announcement of an upcoming feature. Please see the previous post for syntactic aspect of WCPL multiple values support.

Implementing support for returning and accepting multiple values in WCPL is easy: WASM stack architecture makes collecting and spreading the values as natural as passing arguments. In fact, the internal mechanism is exactly the same, so the same code could be used to generate pass-values / return-values code sequences. WCPL already uses the multiple-values convention by representing void functions as WASM procedures returning no values, so extending it to cover 2 and more values is straightforward.

There is one problem though: programmers would expect that multiple values will behave as single values with respect to type promotion, e.g. if one returns an int and a float, but the accepting interface expects, say, a long long and a double, WCPL should generate the appropriate conversion/promotion instructions. It is easy with a single value because instructions work with the value on the top of the stack, but harder if values deeper in stack need to be promoted. WASM has no stack manipulation instructions allowing one to reach deeper into the stack, so WCPL has to generate temporary variables (registers), move the values from the stack to the variables, and then promote them to the new types while putting them back on the stack.

There are also several design decisions that need to be made. WCPL’s multiple values implementation stops short of implementing the following potentially useful features:

  • Expressions returning multiple values. Current implementation allows the return statement and right-hand-side of the multiple values definition or assignment to be an initializer-style display (a list of expressions in curly braces) or a call to a function returning multiple values, but nothing else.

  • Multiple-values casts, e.g. ({double, double})foo(). Automatic promotions are supported, but explicit mv casts are not.

  • Dropping “extra” values as a part of an automatic promotion. WCPL supports “casting to void”, i.e. dropping all return values when mv function call is executed in statement context, but does not allow passing N values to a context expecting K values where K < N unless K is 0.

  • Splicing multiple return values into another call. Current implementation requires accepted values to be stored in variables and other locations, but does not allow feeding them into another function call or operation directly. Some languages allow that, e.g.: bar(x, foo()..., y) splices values returned by foo into bar’s argument list, while (+)(foo()...) adds two values returned by foo.

We will end this post with a complete example, demonstrating most aspects of multiple values manipulation:

#include <stdio.h>

{long long, float} foo() 
{ 
  return {5, 42}; /* vals promoted */
}

{double, float} bar() 
{ 
  return foo(); /* first val promoted */
}

double foobar() 
{ 
  {double a, double b} = foo(); /* both vals promoted */
  return b/a; 
}

double baz() 
{ 
  double da[1], d, *pd = &d; 
  {da[0], *pd} = bar(); /* second val promoted */
  return d/da[0];
}

double quux({long long, float}(*pf)())
{
  {double a, double b} = (*pf)(); /* both vals promoted */
  return b/a; 
}

int main() {
  foo(); /* ignore results */
  bar(); /* ditto */
  printf("%g, %g, %g\n", foobar(), baz(), quux(&foo));
  return 0;
}

Please note that both in function definitions and prototypes the specification of the types for returned values allows naming the values, e.g.:

/* declaration */
static {long long position, float weight} foo();

/* definition */
static {long long position, float weight} foo()
{
  return {5, 42};
}

We don’t recommend this practice for function definitions: the names of the returned values are just ignored by the compiler, but may confuse the reader of the code: they are not variables and can neither be read nor modified.