The PLIB Scripting Language: Applications Guide.
Description: The PLIB Scripting Language (PSL) Library is a lightweight scripting language that is well suited for games or other interactive programs.
Keywords: PSL, PLIB, OpenGL, portable, script, language, Baker, Steve

# The PSL Application Programmer's Guide. By Steve Baker

# Introduction

This document is to help people writing C++ applications to include scripting abilities using the PSL interpreter.

To include PSL scripts into your application requires that you:

       #include <plib/psl.h>

...and link with '`-lplibpsl`'

Then, sometime before you call any other PSL functions, you must call:

       pslInit () ;

Each PSL script is represented by an object of class `pslProgram`

# Example Application

This is the simplest possible PSL application, it loads a script called "test.psl", compiles and runs it.

      #include <plib/psl.h>

      void main ()
      {
        pslInit () ;
        pslExtension extensions [] = { { NULL, 0, NULL } } ;
        pslProgram *prog = new pslProgram ( extensions, "Program1" ) ;
        prog -> compile ( "test.psl" ) ;
        while ( prog -> step () != PSL_PROGRAM_END ) /* Nothing */ ;
      }

Here is a line-by-line explanation:

`pslInit () ;` | Initialise PSL - this must be the first thing you do.
`pslExtension extensions [] ...` | Make a list of PSL extension functions. This program doesn't have any.
`pslProgram *prog = new pslProgram...` | Declare a program (called "MyProgram").
`prog -> compile ( "test.psl" ) ;` | Compile the program to bytecode.
`while ( prog -> step () != PSL_PROGRAM_END )...` | Tell the PSL program to execute one 'step' Keep doing that until we reach the end of the program.

Easy!

Now let's look at `class pslProgram` in more detail.

# `class pslProgram`

Each PSL script is represented by an object of class `pslProgram`.

      class pslProgram
      {
      public:

        pslProgram ( const pslExtension *extns, const char *name ) ;

        pslProgram ( pslProgram         *prog,  const char *name ) ;

        void  setUserData ( void *data ) ;
        void *getUserData () const ;

        void  setProgName ( const char *name ) ;
        char *getProgName () const ;

        void      dump  () const ;
        void      reset () ;
        pslResult step  () ;
        pslResult trace () ;

        int compile ( const char *memptr, const char *fname ) ;
        int compile ( const char *fname ) ;
        int compile ( FILE *fd ) ;
      } ;

# `pslProgram::pslProgram`

There are two constructor functions to choose from. The first takes an array of 'extension functions' and the name by which this program will be known (for error messages and such). The second constructor takes an existing, compiled PSL program and makes a copy of it - it too needs a program name.

The second version of the constructor is especially efficient because this enables the script to be compiled just once - and run multiple times in parallel. The second and subsequent copies of the program consume much less RAM than the first copy because they share the 'code' part of the script.

For example, you can run two copies of a script in parallel like this:

      pslInit () ;

      pslProgram *prog_1 = new pslProgram ( extensions, "code1" ) ;
      prog_1 -> compile ( "data/test.psl" ) ;

      pslProgram *prog_2 = new pslProgram ( prog_1, "code2" ) ;

      while ( prog_1 -> step () != PSL_PROGRAM_END &&
              prog_2 -> step () != PSL_PROGRAM_END )
        /* NOTHING */ ;

# `pslProgram::compile`

When you compile a PSL program, any errors or warnings are reported to stderr (by default) and the number of fatal compilation errors is returned as the result of the 'compile' function. Programs may choose to ignore any compilation errors - but executing the resulting program will immediately produce a PSL_PROGRAM_END.

You can pass to the compiler either:

1. The filename of the file containing the PSL source code...or...
2. A 'FILE *' descriptor for the file containing the PSL source...or...
3. The address of a null terminated string containing the program source PLUS a name to use for the program when reporting errors, etc.

If you pass the filename (1) or the address of the source with a name (3), then error messages from PSL will refer to that name. But if you pass a file destriptor then the program name that you passed to the constructor function will be reported.

Applications that would like to report scripting errors in a more elegant way, may register a callback function that will be called whenever there is a problem within PSL:

      void pslSetErrorCallback ( void (*CB) ( pslProgram *, int, char *,
                                                            int, char * ) ) ;

Your function will be called with five parameters:

      void myErrorCB ( pslProgram *prog, int severity,
                       char *progname, int lineno, char *message ) ) ;

- The 'prog' parameter is a pointer to the pslProgram that had the problem.
- The 'severity' parameter is the error type and severity:
  - PSL_COMPILETIME_WARNING - A problem was found while compiling the PSL program - but it may not be serious enough to prevent it from running.
  - PSL_COMPILETIME_ERROR - A serious problem was found while compiling the PSL program - it won't run correctly.
  - PSL_RUNTIME_WARNING - A problem was found while running the PSL program - but it wasn't serious enough to prevent the program from continuing to execute.
  - PSL_RUNTIME_ERROR - The PSL program 'crashed' while being executed.
- The 'progname' parameter is the name of the program.
- The 'lineno' parameter is the line number at which the problem occurred (if compiling) or the byte-code address at which it failed (if at runtime). The latter isn't much use unless you are a developer of the PSL interpreter.
- The 'message' parameter is the actual text of the error message.

Notice that at compiletime, the 'progname' parameter is the name of the file or string being compiled (if that's known to the compiler). Since it's possible for one PSL source file to '#include' another, you should always use the 'progName' parameter in your error messages in preference to `prog->getProgName()` member function.

# `pslExtension`

We have not yet talked about this mysterious 'extensions' array that's passed into the pslProgram constructor function.

It's important that your PSL scripts are able to interact with your C++ program - and this is done by creating a number of C++ functions that can be called by the PSL program as it executes. These are called 'extensions' because they extend the functionality of PSL.

The extension parameter to the pslProgram constructor is an array of pslExtension structures:

      class pslExtension
      {
      public:

        const char *symbol ;
        int   argc ;
        pslValue (*func) ( int, pslValue *, pslProgram *p ) ;
      } ;

- The 'symbol' field is the name of the extension function AS IT APPEARS INSIDE PSL PROGRAMS. This doesn't have to be the same as the actual name of your C++ function.
- The 'argc' field is the number of parameters the function expects. You can set 'argc' to -1 to allow any number of parameters to be passed - so that you can create functions that work like 'printf' does.
- The 'func' field is a pointer to your C++ function.

The list of pslExtensions is terminated by a `{ NULL, 0, NULL}` entry.

The C++ function has to look like this:

      pslValue my_func ( int argc, pslValue *argv, pslProgram *p )

- The 'argc' parameter to the C++ function is the number of PSL parameters that were passed to it. It's useful to know this number when your pslExtension had 'argc' set to -1.

- The 'argv' parameter is an array containing the parameters that PSL is passing to this function. Each element is a 'pslValue' class which could contain any of the PSL variable types.

- The 'p' parameter is a pointer to the pslProgram that was running at the time this function was called. Notice that pslProgram's can have user data associated with them - so this gives you quite a bit of scope for accessing script-specific data structures.

- When your script has finished doing it's job, it has to return some kind of result. This is another one of those 'pslValue' class objects.

### Example Extension Function

This C++ function prints it's arguments to stdout and returns the value 123.456.

      pslValue print ( int argc, pslValue *argv, pslProgram *p )
      {
        for ( int i = 0 ; i < argc ; i++ )
        {
          switch ( argv[i].getType () )
          {
    	case PSL_INT    : printf ( "%d ", argv[i].getInt    () ) ; break ;
    	case PSL_FLOAT  : printf ( "%f ", argv[i].getFloat  () ) ; break ;
    	case PSL_STRING : printf ( "%s ", argv[i].getString () ) ; break ;
    	case PSL_VOID   : printf ( "(void) " ) ; break ;
    	default : printf ( "Illegal parameter passed to 'print'." ) ; break ;
          }
        }

        pslValue ret ;
        ret.set ( 123.456f ) ;
        return ret ;
      }

Adding this to the example program above, requires only that you change the declaration of 'extensions' as follows:

      pslExtension extensions [] =
      {
        { "print", -1, print },
        { NULL, 0, NULL }
      } ;

Now, now you can write "Hello World" in PSL script:

      int main ()
      {
        print ( "Hello World.\n" ) ;
      }

...and your C++ function 'print' will be called with one parameter that'll contain the string value "Hello World.\n".

### `pslValue`

The 'pslValue' class is used to pass numbers into and out of extension functions. It contains the type and value of a number or string in PSL and it looks like this:

      class pslValue
      {
      public:

        pslType getType   () const ;

        int     getInt    () const ;
        float   getFloat  () const ;
        char   *getString () const ;

        void set () ;
        void set ( int   v ) ;
        void set ( float v ) ;
        void set ( const char      *v ) ;
        void set ( const pslNumber *v ) ;
      } ;

Setting nothing into your pslValue ("my_value->set()") is used to return a 'void' result from your extension function - that is the default type for a pslValue.

The 'getType' call returns the type of this value - currently, it can be: PSL_INT, PSL_FLOAT, PSL_STRING or PSL_VOID. If you 'set' the pslValue, it automatically changes it's 'getType' result to match.

Doing a 'get' for a type that DOESN'T match the 'getType' of the pslValue causes it to try to convert to that type. However, a 'getString' on a non-string pslValue will return NULL. Doing a 'getInt' or 'getFloat' on a PSL_STRING will perform an atoi() or atof() (respectively) in an attempt to get a number from the string.

# Running, Tracing, Debugging.

You run the PSL program one byte-code instruction at a time by calling the `pslProgram::step()` function. A byte-code instruction is rather like the 'machine code' of a physical computer and it typically takes several byte-code instructions to implement each line of PSL source code.

The 'step' function returns one of three possible results:

- PSL_PROGRAM_CONTINUE -- The PSL program is running normally.
- PSL_PROGRAM_END -- The PSL program has finished running. This could be because it simply ended normally - but it could also be because it 'crashed' with a fatal runtime problem of some kind.
- PSL_PROGRAM_PAUSE -- There is a special 'pause' statement in PSL and when that statement is executed, it causes 'step' to return this value.

The PSL_PROGRAM_PAUSE return is intended to cope with the specific case when PSL is being used in an interactive graphical application. Typically, such applications will not want to run PSL scripts to completion every frame - but instead run them up to the next 'pause' statement.

A typical game might have dozens of PSL scripts running in parallel and wish to run each of them until the script 'pause's.

So, your application's main loop might look something like this:

      read_the_joystick () ;

      for ( int i = 0 ; i < num_scripts ; i++ )
        while ( program [ i ] -> step () == PSL_PROGRAM_CONTINUE )
          /* Do Nothing */ ;

      render_the_graphics () ;
      swap_the_doublebuffer () ;

Then, one of those scripts (to move a monster for example) might look like this:

       int main ()
       {
         int i = getMyMonster () ;

         while ( 1 )
         {
           moveMonster ( i ) ;
           pause ;
         }
       }

The 'pause' command indicates that this script has completed it's work for this frame.

Alternatively, some applications may wish to run the scripts for fixed amounts of time, fixed numbers of byte-codes - or until some other criterion is satisfied.

### Resetting a Script

There is a `pslProgram::reset()` function that restarts the PSL program from the beginning having first reset all of its internal variables.

### Debugging PSL scripts

For debugging PSL scripts, you may replace the 'step' call with 'trace' - which causes the byte-code for each instruction to be printed to stderr as it's executed.

You can also call `pslProgram::dump()` to print out all of the byte-code and the PSL symbol table for the program.

### Include paths.

By default, PSL searches for files with relative pathnames in the current directory - but you can override this by setting:

       pslScriptPath ( "directory" ) ;