Second Life Linden Scripting Language Problems

Get your Xah Particle Maker today!
, , 2009-03

This page documents some of the problems of the Linden Scripting Language (LSL).

The LSL is a language with a syntax similar to that of C. The LSL is supposed to be a high-level scripting language (think of JavaScript), in the sense that it supports arithmetic with mathematical vector and quaternion, and is a interactive, event based language that deals with simulations inside a physics engine, to control 3D humanoid characters, movements, building things (architectures, structures, terrains, mechanics …), controlling weather (or the appearance of it)… inside a virtual world computer simulation, and its users are predominantly “game” players who are not professional programers. However, the language actually lacks many of the qualities of high-level languages. Instead, the programer will have to manually deal with data types declaration/conversion, limit in list length, no nesting of lists or any other flexible data types, no library mechanism, no convenient mechanism in parsing strings (e.g. regex), no dynamic evaluation, etc. The following details some of these problems with code examples.

Declaration of Variables and Types

In high-level languages, variables need not to be declared before use. (e.g. LISP, Mathematica, PHP, Python, Perl ) Or, the variable needs to be declared but not with a type. (e.g. JavaScript, OCaml) In LSL, variables must be declared, and with type. Here's a example:

vector offset = <0,0,5>;
float cubeSide = 2.;
string objName = "Xah myObj2";
float rr = 1.;

Manual Casting Of Types

The type casting follows a style similar in other imperative languages, by prefixing a parenthesized type before a value. It can get painful. Here's a example:

 if (p==1) {partyOn(bC,eC, (float) fre, (float) maxAge); llOwnerSay("Okie.");}

 if (cmd1 == "stat") {
   llOwnerSay("Spawn rate:" + (string) (1/fre) + "per second\n" + 
              "Bubble age:" + (string) maxAge + "\n" +
              "Total particles rate per second:" + (string) ( 1/fre * maxAge)
              );
 }

Even in those languages commonly referred to as strictly and statically typed language, often the implicit type casting is more robust than LSL. LSL does not provide automatic type conversion of numbers (integer, float) to string, and there are no functions like “format” or “printf”.

Faulty Automatic Type Conversion

Value must be type casted, and the automatic type casting is not smart.

For example, the “integer” and “float” types are automatically casted into each other, so, one would think there's no difference between 1 and 1.0 in the code since the compiler will automatically convert one to the other. This is not so.

In the following particle system example, if the 1. is replaced by 1, the program will still compile but silently fail.

default {
  state_entry() { 
    llParticleSystem([
 PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,
 PSYS_SRC_MAX_AGE, 0.,
 PSYS_PART_MAX_AGE, 1., 
 PSYS_SRC_BURST_RATE, 1.,
 PSYS_SRC_BURST_PART_COUNT, 2,
 PSYS_PART_FLAGS , PSYS_PART_FOLLOW_SRC_MASK
 ]);
  }
}

The technical excuse is that the compiler do not automatically see types inside a list. (but it does, for example, automatically convert float or integer inside vectors)

Primitive List Handling

List and list processing is a hallmark of high-level languages. In such languages (Perl, Python, JavaScript, LISP, Mathematica …), either there's a general list system where the list elements can be lists themselves with no restriction on type or nesting level or number of items, and or, the language provides many other list types such as keyed lists (aka associative list, hash table, dictionary, maps), sets, n-tuples (pairs, vectors, arrays), matrixes, … etc for specialized purposes or algorithmic properties. LSL provides a list type that can have elements of arbitrary type. (aka heterogeneous list) Other than this, LSL's list facilities is extremely limited. Here are some problems:

Inflexible List Element Extraction

List elements in LSL cannot be easily extracted. For example, in high-level languages, list element extraction take a form like this elem3 = myList[3] or elem3 = take(myList,3) for taking the third element from the list. But in LSL, partly due to the strict requirement of type matching, one has to know the type of the element before extraction, and must use the right function for different types. So, in LSL, to take the element of third index of a list, it would be: elem3 = llList2Float(myList,3) or elem3 = llList2Integer(myList,3) or any one of the following other functions depending on the type of the element to be extracted: llList2Key, llList2String, llList2List, llList2Rot, llList2Vector.

The need to know the type in advance to call the right function to extract a element of a list severely restricts the usefulness of heterogeneous lists. (llGetListEntryType is provided for finding out the type of a element)

List Has Limitations In Length

LSL list has a limitation in length, in the sense that a programer cannot start with a list with more than 72 items.

In order for programer to have a list of more than 72 items, she must create 2 or more lists and join them.

No Nested List

LSL does not provide a keyed-list (aka: associative list, Hash, Dictionary, Map), nor any other data structure that is nested list in nature. (e.g. trees, matrices) Some of these limitations can be worked around. For example, a list can contain element of type vector, and vectors's parts can be accessed individually. This provides a form of a rectangular nested list (a matrix), with elements being floats. For a keyed list, one can work around by building a flat list of alternating keys and values. But overall, the lack of flexible list structure is a severe limitation.

No Dynamic Evaluation

In interactive or artificial intelligence applications, such as interactive visualization systems, computer algebra system, interactive human language processing system, knowledge-base system, etc., it is often required that the language can evaluate code in a dynamic way. Namely, that the language should be able to construct a snippet of code on the fly and evaluate that code as part of its computation. (Perl, Python, JavaScript, … have the function eval(), and LISP and Mathematica take the step further by dealing with symbols directly.) Second Life's virtual world environment is a highly interactive environment, where objects interact with users inside the virtual world at all times. The interaction may be in the form of touching, collision, proximity, or “spoken” (typed) commands from nearby users. However, LSL does not provide dynamic evaluation ability. This severely limit the power of LSL. Here's a example:

In Second Life, the standard method for a object to react to commands from a user is by listening to what the user types. For example, i may create a object that when a user types “c1”, where c1 is any one of a list of color names such as “pink, orange, brown, purple, gold”, then the object will change its color accordingly. In the source code, i may define these colors:

vector pink= <1,0.07843,0.5765>;
vector orange= <1,0.6471,0>;
vector brown= <0.5451,0.1373,0.1373>;
vector purple= <0.3333,0.102,0.5451>;
vector gold= <1,0.8431,0>;

And suppose the user's command string is parsed, so that i have a variable named “cmd” with value of a string "pink" that is given by user. Now, it would be nice, if i can simply evaluate this as a variable, to obtain the actual RGB vector value for color. Typically, something like myRGB = eval(cmd). The cmd would evaluate to "pink", which evaluate to <1,0.07843,0.5765>. But LSL does not provide any dynamic evaluation abilities.

To work around this, a keyed-list for the colors may be used. For example, colorList=[pink:rgb1,orange:rgb2,…] and myRGB=getValFromKey("red",colorList). However, LSL does not provide keyed-list neither. To work around that, perhaps a programer can implement a keyed-list by having a list who's elements are lists of 2 elements, but LSL doesn't allow nested list neither. Nor does LSL provides any sort of Object Oriented programing system, so that a color list can be stored as a object with properties, and individual values can be retrieved by accessor methods. All in all, this makes programing in LSL fairly tedious and severely limits what it can do.

No Library System

In modern languages, there is a library system, where the programer can define a set of functions in a file for a particular purpose. This file can be loaded into the main program, as to make the functions available for use in the main program. This mechanism is variously known as a library, module, package. This allows the programer to build code libraries that can be reused or distributed.

A library system usually involves a name space mechanism with functions to import files, and or sometimes simply a file loading mechanism (commonly named “require”, “include”). In LSL, there is no name space, nor any facility to load a separate source code. Every function must be defined inside a script. A script cannot call functions that are inside another file (and there is a limit of 65k bytes on a script's file size).

This makes it difficult to build any substantial program in LSL. For example, one might want to write a chess-playing machine in Second Life. Since Second Life is a virtual world, a chess playing machine in Second Life is ideal, since in this virtual world system the programer can actually create pieces that moves by themselves, or animate human players moving chess pieces. However, because the lack of a library system, combined with the primitive nature of LSL, the cumbersomeness to create a substantial algorithm such as chess playing code, makes it practically impossible.

As a virtual world environment, there are huge potential on what computer programs could emulate in this world. For example, in diverse fields: language translation, voice recognition, voice synthesizing, music synthesizers, robotics, mathematical visualization systems, 3D cellular automatons. A primitive language with no library system makes it practically impossible to create substantial algorithm or programs.

(LSL does provide communication functions via HTTP to outside servers. This is intended so that LSL programs only act as simple programs or interface, while programs requiring intensive computation (such as a chess engine) should be done by servers outside of the sim. However, this is a matter of restricting use of resources, and LSL already has lots of artificial limits, including file size, number of scripts per prim, and memory allotted per script. The memory space can already serve as a resource limit for loading libraries. Limiting resources is not a good reason for not providing a library system.)

No Regex Support

In all modern scripting languages, regex is build in. For example: Perl, JavaScript, PHP, Python, emacs lisp, Ruby. However, in LSL, there is not any support for pattern matching on text. Given that a primary way for objects to interact with users is by texts that user typed or strings other script sent through communication channels, lacking regex is a major usability problem of the language.

Code Inside Event Handler Only

LSL does not allow any computations outside of state blocks. For example, the following code is a syntax error. However, if the 2*PI is replaced by PI, then it will compile.

float uMax = 2*PI;
default { state_entry(){} }

LSL does not allow any code inside a state block but outside a event handler block. For example, the following code is a syntax error. However, if the integer i=0; line is moved inside state_entry, then it is OK.

default {
  integer i=0;
  state_entry(){}
}

Peculiar Form of Accessing Event Handler's Properties

LSL's event handlers have a peculiar way to pass predefined properties that is very confusing as function definition and function call.

For example, “touch_start” is a event handler. It is invoked when a user touches a object. This event handler needs a way to pass the info about the agent that touched the object. Typically, in event based languages, these info are done as properties of the event handler. For example, “touched_start.toucher” may return the id of the player who touched it.

In LSL, the properties of event handlers are done in the form of function definition of its parameters. Here's a example:

// on touch, give out the note named “store info” to the toucher.
default
{
    touch_start(integer num_detected)
    {
        integer i;
        for(i = 0; i < num_detected; ++i)
        {
            llGiveInventory(llDetectedKey(i), "store info");
        }
    }
}

Here, touch_start has one property, which is the user who touched it. This info comes in the form of a integer. (programer can call other functions and pass this integer as argument to get the actual UUID of the toucher, or name of the toucher) The way to call this event handler is this form: touch_start(integer ‹name›), where the “‹name›” is arbitrary. (the name “num_detected” is the default name used by LSL documentation, but it can be any thing.)

Note that this form of event handler property is confusing, because its form is similar to a function definition, but its use is similar to a function call, while actually it is neither. Generally speaking, given a event handler:

event_handler(type1 name1, type2 name2, …)

Programer cannot alter the type declaration there, nor adding more parameters, nor supplying arbitrary values for the names as if they are arguments to a function.

It appears to me, this parametrized form of event handler calling, together with the parameter types, is designed so that LSL programers can easily see in the source code all properties predefined by the event handler, and the types of each.

Note that in many cases, the semantic of the event handler's properties itself is very low level, non-intuitive, or badly designed. For example, in the case of touch_start, the info of the toucher is provided in the form of integers, in the sense that first toucher is 1, second toucher is 2, etc. There are several problems with this. The event handler should be invoked each time there is a touch, instead of giving multiple numbers of presumably simultaneous touch. If we assume, for some historical implementation reasons, that the event handler is unable to be invoked per touch incidence, then it could provide the touchers property as a list. Also, there is no logical reason for the toucher info not to be in the form of UUID or agent names directly. Almost always, the programer will need to call function to turn the integers to user id or names anyway. The argument for saving computing resources does not seem valid.

There are a total of 33 event handlers. 25 of them have event properties (parameters). Of these, 7 of them have the form event_handler_name(integer num_detected).

Use Of Low-Level Language Concept Of Bit Mask

The low-level language's concept of bit mask is heavily employed in LSL. For example, it is used for the parameter PSYS_PART_FLAGS in the function llParticleSystem, as well for a Second Life 3D prim's “param”, as used in on_rez, llRezObject, and many related functions dealing with prims.

Often, a function will need to take many True/False parameters. For example, suppose i have a function that can draw a rainbow, and each color of the rainbow can be turned on or off individually. My function specification can be of this form: rainbow(red, orange, yellow, green, blue, violet, purple). Each parameter is a true or false value. So, to draw a rainbow with only red and yellow stripes on, one would code, for example rainbow(t,f,t,f,f,f,f), where “t” stands for true and “f” stands for false. (or, similar values for the true/false of the language's boolean system)

The problem with this simple approach is that when a function has many parameters, “which position means what” becomes difficult to remember and manage. Alternatively, a high-level language may provide a system for named parameters. So, for example, the function may be called like this with 2 arguments rainbow(red:t, yellow:t), meaning, give the true values to the parameters named “red” and “yellow”. Parameters not given may automatically assumed to have false values by default. Similarly, the language's function call can look like this: rainbow(red, yellow), where omitted parameter names simply means false.

LSL deals with this issue by using a concept of bit-mask that came from low-level languages. From the programer's point of view, the way to call this rainbow function would look like this: rainbow(red | yellow). On the surface, it seems just a syntax variation. But actually, the red and yellow here are global constants of type integer, defined by the language, and the | is actually a bit-wise binary operator. To explain this to a educated person (e.g. a mathematician) but who are not a professional programer, it gets a bit complex as one has to drag in binary notation, boolean operation on binary notation realized as a sequence of slots, and the compiler ease in processing numbers as binary digits, and the compiler writer and language designer's laziness in resorting to these instead of a high-level interface of named parameters.

The hack of using the so-called bit-mask as a interface for functions that need named parameters, is similar to languages using “1” and “0” as the true/false symbols in its boolean system, and languages using the “or” operator “||” as a method of nested “if else” program flow constructs. The problem with these hacks, is that they jam logically disparate semantics into the same construct. Their effects is making the language more difficult to learn, source code more difficult to read, and thus increased programer error and maintenance.

Abuse of CONSTANTS

LSL has 238 predefined constants. Vast majority of these constants are used as a hack for LSL functions's pre-set parameter values. For example, the function llParticleSystem uses 34 constants, and the function llSetPrimitiveParams uses 54 constants.

default
{
    touch_start(integer num_detected)
    {
        llSay(0, (string)LINK_ROOT); // will output “1”.
    }
}

For example, here's the constants for the function llParticleSystem.

PSYS_PART_BOUNCE_MASK, PSYS_PART_EMISSIVE_MASK, PSYS_PART_END_ALPHA, PSYS_PART_END_COLOR, PSYS_PART_END_SCALE, PSYS_PART_FLAGS, PSYS_PART_FOLLOW_SRC_MASK, PSYS_PART_FOLLOW_VELOCITY_MASK, PSYS_PART_INTERP_COLOR_MASK, PSYS_PART_INTERP_SCALE_MASK, PSYS_PART_MAX_AGE, PSYS_PART_START_ALPHA, PSYS_PART_START_COLOR, PSYS_PART_START_SCALE, PSYS_PART_TARGET_LINEAR_MASK, PSYS_PART_TARGET_POS_MASK, PSYS_PART_WIND_MASK, PSYS_SRC_ACCEL, PSYS_SRC_ANGLE_BEGIN, PSYS_SRC_ANGLE_END, PSYS_SRC_BURST_PART_COUNT, PSYS_SRC_BURST_RADIUS, PSYS_SRC_BURST_RATE, PSYS_SRC_BURST_SPEED_MAX, PSYS_SRC_BURST_SPEED_MIN, PSYS_SRC_MAX_AGE, PSYS_SRC_OMEGA, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE, PSYS_SRC_PATTERN_ANGLE_CONE, PSYS_SRC_PATTERN_ANGLE_CONE_EMPTY, PSYS_SRC_PATTERN_DROP, PSYS_SRC_PATTERN_EXPLODE, PSYS_SRC_TARGET_KEY, PSYS_SRC_TEXTURE

This use of constants is just abuse. It is a extremely lazy way of designing a function. For a example of how LSL functions typically use these constants, see: wiki.secondlife.com LlParticleSystem.

Misc

In general, LSL is very buggy. One of the bug in 2007 is that

integer i = 3 -1;

is a syntax error, but

integer i = 3 - 1;

is OK. (this bug has since been fixed.)

LSL does not have functions like isFloat() that determines the type of a variable/value.

LSL has a function llAbs() to get the absolute value, but it works for integers only. For float, one has to use llFabs(). Together with the fact that there is no function to get a data's type, this makes much of dynamic coding very difficult.

A multitude of minor problems like the above adds to the pain of LSL.

Want to dash through walls?
Try Xah Tele-Dasher!
blog comments powered by Disqus