SPEL: Sony Pictures Expression Language

Spel was developed as a scripting language for intializing and manipulating the state of the Sandstorm simulation system. Its primarly design goal is to offer an efficient and easy-to-use interface to c++ class objects through a scripting language that is simple yet powerful. Originally the project used Python as the primary glue for providing artists with an interactive script-based interface to its library of simulation tools but, while Python is an impressively powerful language in its own right, its API for interfacing to C++ proved to be too inefficient and awkward.Also with Spel, vector and matrix variables are built-in types which increases the efficiency of vector and matrix based computations.

Spel is a tokenizer which means that it first parses the script into tokens which are then evaluated at runtime. This makes it faster than a purely interpretive language but not as fast as a compiled language. However it is simplier to use than a compiled language and it is platform independent. Its syntax is very "c-like" although there notable departures from c:

Comments

Comments are done c-style (/* ... */) or c++ style (//).

Vectors, Matricies, and Strings

The variable types vector and matrix are built-in variable types and have some common properties. They both are converted to floats or ints by taking their first component. In this way they can be passed to a function that only works on floats such the sin() function. If a conversion from a int or float is needed then that value is used to set all the components of the vector or matrix.

Components of vectors can be accessed via the following attached methods:

Matricies and strings differ from the other built-in types in that they are not arrays although their internal components can be accessed as if they were elements of an array. In the case of a string, the internal components are single letter substrings.

Matricies have the following attached methods:

Strings can be combined with numbers and other strings via the concatination operator (+). For example:

float a= 1.2;
echo("The value of a= "+a);

Arrays

Variables of type int, float, or vector are treated as arrays whether or not they are defined with an array dimension. Arrays can be resized without losing their current contents. For example the following code is valid:

  vector v= vector(-2,4,7); //define a vector
  v.resize(3);     //expand the v vector array to includue two more vectors
  v[2]= vector(7,8,9);  // vector array v now holds the values: (-2 4 7) (0 0 0) (7 8 9)

Note that indexes are zero based meaning the first member of the array has an index of 0 (not 1.)

Also note that vector components can not accessed with an index as is the case with many c++ vector classes.

Arrays have an internal iterator which identifies which element of the array is being reference when there is no explicit index. For example:

float a[100]= 5;
a.setIndex(2);
a= 123; // This is the same as a[2]= 123;

The following methods can be used to change the internal iterator state or check its current position or the array size:

Loops

Traditional c-style "for" and "while" loops are supported although "do" loops are not.

A new loop statement was added to support iterators. The following is an example of its usage:

  float a[4];
  int i= 0;
  loop( a.first(); a.next() ) { 
    a= i; 
    i+= 1; 
  }

The "loop" statement is a little more efficient than a "for" loop for iterating over an array.

As in c, a break statement can be used to terminate loops.

Built-in Constants

The following are built in constants which can not be overriden with a variable or function name:

Also constants are defined for the various variable type values returned by the type() function described below: FLOAT, INT, VECTOR, MATRIX, STRING, UNKNOWN

Built-in Functions and Operators

The following operators have been implemented:

   , ;             comma and semicolom operators for seperating statements
   +, -, *         add, subtract or negate, and multiply
   /               divide with the right hand value treated as a float
   <, >            relational operators -- they return an int value
   ==, !=, <=, >=  two digit relational operators
   && ||           boolean and and or operations.
   ^               dot product for vectors, pow() for floats and ints
   %               cross product for vectors, modulo for ints
   ? :             conditional evaluation operator

The following assignment operators have been implemented:

    =, +=, -=, *=, /=

The following standard math functions have been defined -- note that trigometric functions work with radians as opposed to degrees and all return a floating point value:

    abs(n), acos(n), asin(n), atan(n), atan2(n1,n2), ceil(n), cos(n), floor(n), pow(n1,n2), round(n), sin(n), sqrt(n), tan(n)

There are built-in functions designed for displaying and converting variables:

Additional functions:

The rand() function can be called with no parameters or up to three paramters. The forms are as follows:

If rand() is called with no parameters then it returns a float value. If it has one or more parameters and the first parameter is a vector then the returned value is a vector otherwise it is a float value. If it has three parameters then the last parameter is a seed value which is a float value that is used to seed the random number generator unless this parameter is zero, in which case, the current time is used to seed the random number generator. The random number generator only needs to be seeded once to generate a repeatable sequence of values however reseeding with the seed parameter set to zero will cause it to continue to use the current time for reseeding. The standard math library function grand48() is used to generate these numbers.

Defining subroutines within a script

Subroutines (i.e. functions) are defined in a perl-like manner without named parameters or a defined return type. Instead parameters are passed as values in an array and any of the built-in data types can be returned. To illustrate with a very simple example:

  /* Create a new subroutine that takes a single parameter. */
  sub add6(1) {
    return _arg[1] + 6;
  }

  int x= 3;
  int y= add6(x);
  echo(y);

In this trival example we see that a subroutine definition begins with the keyword sub followed by the subroutine name add6, the number of parameters expected by the subroutine (1), and then the subroutine body inclosed in curly brackets. The parameter quantity specifier is optional. Without it any number of parameters can be passed. The value of the passed parameters are held in the array named _arg with the quantity in the zero indexed element, the first value in the next element, the second in the element after that and so forth.

Subroutines can define local variables that are only accessable within the subroutine or they can access external variables. To illustrate with an example:

  /* Prototype a subroutine that no input parameters. Its full definition is at the bottom. */
  sub add6ToX(0);

  /* Create another subroutine that accepts a variable number of parameters.
   * Use the static nature of local variables to keep track of how
   * many times this subroutine has been called.
   */
  sub echoParams {
    int cnt += 1; // Increment local variable each time subroutine is called.
    echo("echoParams has been called "+cnt+" times.");
    echo("It is being called with "+_arg[0]+" parameters. Their values are:");
    while( _arg.next() ) echo (_arg);
  }

  int x= 3;
  int y= -10;
  add6ToX();
  echoParams(x, y);

  /* Here we define the body of add6ToX such that it modifies
   * the external variable "x" declared above.
   */
  sub add6ToX {
    int y= x + 6;
    x= y;
  }

Here a subroutine called add6ToX is created which takes no input parameters but modifies an external variable named x. Notice that it declares a local variable named y which is the same name used by an external variable. Because it was declared inside the subroutine it is a different variable than the external one. Also note that the subroutine is declared twice: once at the top of the script and again at the bottom. The body of the subroutine is defined at the bottom because you can not access variables in a script before their definition. However you also can not use subroutines before they are declared. Consequently the add6ToX subroutine is declared at the top of the script and defined at the bottom.

Another thing to note about this definition of add6ToX is that it does not include a "return" statement. The result of the final statement is returned if there is no explicit "return" statement at the end.

A second subroutine called echoParams is also defined. It uses an iterator to cycle through the values passed in the _arg array and print them out. Since its definition does not include a parameter quantity specifier it can take any number of parameters. It also prints out a count of how many times it has been called. It can do this since local variables are static meaning they will retain their last assigned value.

Catching run-time errors

Spel supports c-like try and catch syntax to catch errors at run-time. Here is an example:

  try {
   int i= 0;
   int j= PI/i; //division by zero error
  } catch(string err) {
    echo(err);
  }

In the above script, if an error should occur during run-time then the variable in the catch declaration will recieve an error value and the code in the catch block will be executed. This variable may be either a string type or a numeric type. If it is a string type then it will be set to a string based representation of the error (if one should occur) while a numeric value will get the integer error code.

Optimizing your script

The Spel parser does not do much in the way of optimizing so it pays to be efficient in how you write your code. One thing the parser will do is precompute arithmatic expressions that contain only constants. For example:

  for ( int i; i < 6; i+=1 )
    r += i * (PI/2);

The Spel parser will precompute (PI/2) since this subexpression contains only constants. However if the expression had not contained parenthesis (i.e it had been: i * PI/2) the parser would not have detected the optimization. Consequently it is a good idea to place parenthesis around constant expressions.

Using Spel via the SpelPointOp

A Sandstorm object called SpelPointOp was created to facilitate the using of Spel in the manipulation of particles. Assuming that you are already familiar with Sandstorm PointContext objects the following code demonstrates how the SpelPointOp works:

  ParticleSim psim;

  Attribute custom("custom", sizeof(float) * 2, GPD_ATTRIB_FLOAT);
  psim.points().addAttr(custom);

  PointContext ctx( psim.points(), psim.pointGroups(),
                    psim.idList(), psim.partIndex(), psim.maxId() );
  SpelPointOp pointOp;
  pointOp.setScript( Script.c_str() );
  pointOp.exec( ctx );

The above code assumes that a std::string object named Script exists which holds the Spel script. A blank pariticle object named psim is created and an attribute named custom is added to it. Then a PointContext named ctx is created which holds psim particle tables.

Finally a SpelPointOp named pointOp is created and it is given the script and context. The script is parsed and executed in the exec method. Here is a copy of a simple Spel script which will initialize the particle tables:

/* Resize the point list and intialize values
 */
_ctx.addAttr("PSqr", VECTOR)
_ctx.resize(3);
int ii= 0;
loop( _ctx.first(); _ctx.next() ) {
  //assign values using local names
  _P= vector( ii+=1, ii+=1, ii+=1 );
  _v= ii += 1;
  _custom[0] = noise(10.1,ii+=1);
  _custom[1] = noise(10.1,ii+=1);
  _PSqr= _P * _P;
}
_ctx[2].v= _ctx.PSqr;

The SpelPointOp object has created a object named _ctx which exposes the particle attributes to Spel scripts and this is used to call an associated addAttr method to create a new attribute named PSqr and then the resize method is used to create 3 particles (the original object had none.) The loop statement uses the _ctx object's internal iterator to cycle through these particles and assign values to particle attributes P, v, custom, and the newly created PSqr. The Spel objects _P, _v, _custom, and _PSqr provide read and write access to these attributes.

The last line of the script demonstrates an alternative format for accessing attributes. The _ctx[2].v subexpression provides a reference to the v attribute of particle 2 and _ctx.PSqr references the current PSqr value. Note that _PSqr and _ctx.PSqr are two names for the same thing.

The following are methods which belong to the _ctx object:

-- Main.paulvc - 07 Dec 2005