Firestorm LSL Preprocessor
Overview
Developing scripts in LSL can be tedious, especially if you are creating scripts that use the same things over and over again, and you keep copying pieces of your older scripts to new creations. And even within one project, you often have to copy parts of your scripts into sub-scripts (example: link message numbers as identifiers for commands). Changing any of these numbers or updating and fixing bugs in older variants of the code might result in different versions of the same functions being used or in bugs found in older versions not being fixed in newer creations. The Firestorm LSL Preprocessor is a tool to help you circumvent a lot of these problems.
Adding and removing debugging statements is another thing with which the Preprocessor can be helpful. Usually you have debugging functions in the script to see if all is working fine, and you take them out before release. But this in itself creates new places for mistakes as well as the opportunity for some debugging output to remain in the script on release day because it was overlooked during final screening. Using the Firestorm LSL Preprocessor gives you a very simple way of making sure that no debug output is left in your final release.
Setup
To enable the LSL Preprocessor, open the Preferences panel (Ctrl-P), then Firestorm -> Build 1. Mark the checkbox “Enable LSL Preprocessor”. Enable or disable the Preprocessor options according to preference:
Script Optimizer
switch() statement
Lazy lists
#includes from local disk. This last enables the field below, Preprocessor include path. Click the Browse button to open a file picker, which allows you to select the folder where you store all your LSL include files.
How it Works
After setting up, you will see two tabs in your LSL editor: “Script” and “Preprocessed” (“Postprocessed” in older versions). The first is your active scripting window; the second shows the output of the Preprocessor, essentially the content that gets stored in the script. The processed script contains the entire source code of what you have written in a comment block at the beginning, and the processed output following this comment block. This ensures backwards compatibility with older versions of Firestorm and other viewers that do not support preprocessing at all.
NOTE: Since the LSL/Mono compiler gets the script after preprocessing, the line numbers in error messages refer to the processed script. So if you are getting compiler errors, be sure to look for the error in the “Preprocessed” tab rather than in your original script.
A Short Example
Create a file in your include folder with the name debug.lsl. Copy the following snippet into this file:
#ifdef DEBUG
debug(string text)
{
llOwnerSay(text);
}
#else
#define debug(dummy)
#endif
Now create a new LSL script and copy the following code into the script editor:
#define DEBUG
#include "debug.lsl"
default
{
state_entry()
{
debug("Debugging with the Firestorm LSL Preprocessor.");
}
}
Save the script. You will see the “Debugging with the Firestorm LSL Preprocessor” message in your chat console. Now change the first line of the LSL script to:
#undef DEBUG
Save the script again. You will see that the debugging message is gone. This is a very handy way to enable and disable all debugging code in one single line. To understand what really happens, have a look at the “Preprocessed” tab. You can clearly see how the debug line will simply disappear, a lonely “;” the only trace of it having been there.
The Optimizer
Including files has one disadvantage though. You will get the complete contents of the file, if you need it or not. But the Firestorm LSL Preprocessor uses an optimizing technique, which only keeps the things you really used in your code and removes all global functions and variables you didn't reference in your script. This makes sure that your scripts don't get burdened with a lot of unused code.
You can enable or disable this functionality with Preferences→ Firestorm -> Build→ Script optimizer.
Switch/case Addition
The Preprocessor also adds a set of new commands to the LSL editor which has been sorely missing until now: the switch/case construct known from many other languages. You can enable support for this construct with Preferences→ Firestorm -> Build→ Switch() statement.
Alternately, you can include a USE_SWITCHES macro at the top of the script to enable this:
#define USE_SWITCHES
switch/case is a handy replacement for if(…) else if() chains. Additionally, switch/case supports “fallthrough” from one case to another, so you can chain up several cases with different conditions. A break statement is used to prevent fallthrough. The default case is used if none of the cases match the switch() condition.
Example:
default
{
state_entry()
{
integer i;
switch(i)
{
case 1:
{
llOwnerSay("1");
// fallthrough to case 2
}
case 2:
{
llOwnerSay("1 or 2");
// no fallthrough
break;
}
case 3:
{
llOwnerSay("3");
// fallthrough to default
}
default:
{
llOwnerSay("3 or default");
}
}
}
}
Note that the colon ':' after default or case is not needed if a block (an opening curly brace { introducing a series of statements) comes immediately next. For example:
switch(x)
{
case 1: // needs colon
case 2 // colon optional as curly brace opens next
{
llOwnerSay("x is 1 or 2");
break;
}
default // colon optional as curly brace opens next
{
llOwnerSay("x is neither 1 nor 2");
}
}
Lazy Lists Addition
Assigning values to list indexes is always a cumbersome thing to do in LSL (llListReplaceList() needed). Lazy Lists can help you a little by providing a way to just say:
myList[index]=value;
To obtain a list element, prefix it with a typecast. For example:
list a = [1, 3, "blah", <5.31, 131.7, 11.331>];
llOwnerSay((string)a[2]); // outputs: blah
llOwnerSay((string)((vector)a[3])); // outputs: <5.31000, 131.70000, 11.33100>
llOwnerSay(llList2CSV((list)a[1, 2])); // outputs: 3, blah
will be translated to:
list a = [1, 3, "blah", <5.31, 131.7, 11.331>];
llOwnerSay(llList2String(a, 2));
llOwnerSay((string)(llList2Vector(a, 3)));
llOwnerSay(llList2CSV(llList2List(a, 1, 2)));
The function that sets elements, lazy_list_set(), can be user-overriden. The prototype should be:
lazy_list_set(list target, integer index, list value)
With the current function, setting elements beyond the current list length makes the intermediate empty spaces be filled with integer zeros. That may not be desirable in some applications (e.g. another default value may be required), so overriding the function can be necessary.
You can enable or disable this functionality with Preferences→ Firestorm -> Build→ Lazy lists, or with a macro in the script itself:
#define USE_LAZY_LISTS
Preprocessor Commands and Macros
The Preprocessor understands the following commands:
#define
#undef
#ifdef
#ifndef
#if
#elif
#else
#endif
#warning
#error
#include
There are a few more, but these are not really useful within LSL.
Additionally, you can use the following macros in your scripts to help with debugging and giving you other useful information:
__FILE__ - the full path to the script as it would appear in the include cache. The top script only uses its name.
__LINE__ - the line of the current script where it is expanded; this starts at line 0
__SHORTFILE__ - the name of the current script without full file path
__AGENTID__ - a string-encapsulated version of the agent's key who compiles the script
__AGENTKEY__ - same as above, legacy version
__AGENTIDRAW__ - a nonstring-encapsulated version of the agent's key who compiles the script
__AGENTNAME__ - a string-encapsulated version of the agent's full name who compiles the script
__ASSETID__ - a string-encapsulated version of the assetid of the current script; may return “NOT IN WORLD” or a nonstring-encapsulated null key in rare circumstances
Thanks to Zwagoth Klaar for this list. __FILE__ and __LINE__ come from the Boost::wave library.
#define
#define creates a case-sensitive macro which will be replaced while saving and compiling a script. This can be applied as simple constant numbers, strings, and even functions. What happens is a literal text replacement in the source code. Because of this, be careful with “;” inside your macros. These usually don't cause any harm; but within a one-line conditional, things can break in unexpected ways if it creates a “;;” in the end of a line, for example.
The following examples will give you an overview of what #define does. Have a look at the “Preprocessed” tab to see what the Preprocessor creates from the source.
Example 1:
#define CHANNEL 12345
llOwnerSay((string) CHANNEL); // CHANNEL will be replaced with the literal 12345 stated in the #define above
#define can also take parameters to apply to the replacement code:
Example 2:
#define OS(b,c) llOwnerSay(b+c)
OS("Test","123"); // will expand to: llOwnerSay("Test"+"123")
Example 3: Making strings out of parameters
#define OS(a) llOwnerSay(#a)
OS(1234); // will expand to: llOwnerSay("1234");
Example 4: Using ## to concatenate parameters
#define OS(a,b) llOwnerSay((string) a##b)
OS(1234,5678); // will expand to: llOwnerSay((string) 12345678);
Example 5: Using \ to make multi-line macros
#define OS(a,b) if (a > 1) {\
llOwnerSay((string)a);\
} else {\
llOwnerSay((string)b);\
}
OS(1234,5678); // will expand to: if (1234 > 1) { llOwnerSay((string)1234);} else { llOwnerSay((string)5678);};
Single-line comments, \\, will cause undesired effects. Use multi-line comments instead, /* */.
#undef
Removes a macro previously set up with #define. If the macro was not made in the first place, nothing happens. This is a useful way to enable or disable parts of the source code for debugging. See #define above for an example on how to use it.
#ifdef and #ifndef, #else and #endif
This command is a part of conditional preprocessing in association with #else and #endif. #ifdef checks if a macro has been previously #defined. It doesn't matter if the macro has actually a value assigned to it. It just needs to be #defined. If it has, all of the code after #ifdef up to #endif or #else gets replaced into the code. #ifndef does the exact opposite. If a macro does not exist, the code goes into the Preprocessor.
Example:
#define OWNER_ONLY
...
#ifdef OWNER_ONLY
key var=llGetOwner();
#else
key var=llDetectedKey(0);
#endif
#if and #elif
These are also conditional preprocessing commands. They take a general condition and pass on the code to the Preprocessor if the condition evaluates to TRUE. They can also be used in conjunction with #else. #elif is the equivalent to else if.
Example:
#define DEBUGLEVEL 2
#if DEBUGLEVEL==1
llOwnerSay("Point reached");
#elif DEBUGLEVEL==2
llOwnerSay("Lots of more data here");
#else
llOwnerSay("Unknown debug level: "+(string) DEBUGLEVEL);
#endif
#warning and #error
These two commands show a string in the compiler window to warn you about certain problems or to halt compilation immediately due to a fatal error. Right now, both #warning and #error cause the compiler to stop. It is unclear yet if #warning will allow the compiler to continue to completion in the future.
NOTE: If one of these commands is hit by the Preprocessor, the script is NOT saved!
Example 1:
#warning This include file is obsolete!
Example 2:
#error This include file does not work anymore. Please update.
#include
This is probably the most powerful feature of the LSL Preprocessor. It includes whole source code files from your harddisk or from the same folder tree in your inventory into the script you are working on. A small example of this feature can be seen above.
#include takes a file name relative to the include path set up in Preferences→ Firestorm -> Build→ Preprocessor include path. You can also include files inside of subfolders. If you are compiling your script from your inventory, the Preprocessor will search the inventory path you are working in, descending into any subfolders, to find the referenced include file. Unused functions and global variable declarations are removed by the Optimizer.
Relative paths, ./ and ../, may also be used. Useful for projects without requiring static top level folder names.
Examples:
#include "command_ids.lsl"
#include "general_functions.lsl"
#include "hud/layout.lsl"
#include "../generic_lib.lsl"
#include "./classes/lawmower.lsl"
NOTE: Be careful about including files from within #includes. You might #include a file twice, if you are not really keeping track, and this will lead to problems. This issue is usually addressed by using so-called “Include guards”. You basically have a conditional compile that sets a #define macro and checks if it's already there. If it's not, #include the contents of the file. If it was set, ignore the contents.
Example:
#ifndef SCRIPT_NAME_LSL
#define SCRIPT_NAME_LSL
your_script_starts_here()
{
}
#endif //SCRIPT_NAME_LSL
These #ifndef, #define and #endif commands make sure that the #include happens only once regardless of how often the file is actually referenced with #include.
Type Casting
There are some predefined macros in the preprocessor that allow you to use simpler type casting in LSL. For example:
Known Issues
Including a file from your hard drive containing a #endif as last line (without linebreak) will not produce a warning about not having a line break but rather a statement error
This can be fixed by adding a line break to the end of the file
“Enable Text Compress” will break your script! (At least in its current form. It may be fixed, or removed, in the future.)
External links