Lua FAQ
Can I keep using LSL?
Yep! LSL should work exactly the same as before, and a lot of testing work has been done (and is being done) to ensure that is the case. Even if you’re an LSL pro and rely on things like (some_list != [])
for getting the list length, or strange things like ((largestring="") + largestring)
, you’re fully covered. Those still work, and now have optimized implementations.
In fact, LSL will get better than ever before, running faster, and using less memory! The generated bytecode for an LSL script in Lua is generally half the size of a Mono script, and most strings take up half as much memory by virtue of using UTF-8 instead of UTF-16.
We’ve taken a lot of care in writing the new LSL compiler to make the scripting experience for people who prefer LSL better than it was before.
Are you going to stop adding functions / events to LSL?
In the general case, no. A big part of the work we’ve done is making it easy to share functions between LSL and Lua without much extra effort. For events and functions that can be reasonably exposed to LSL (i.e., they only use simple types that LSL has anyway) then LSL should still get those.
For more advanced features, the idea is that there should still be a path for exposing them to LSL. Obviously, with Lua it’s possible to have object-oriented APIs that would not be possible in LSL, but those objects will generally be written in a way that calling a method on a “special” object we introduce in Lua will really boil down to a function call that can be expressed in LSL.
Not a real example, but some_object:giveInventoryList("folder_name", inventory_list)
might really boil down to a llGiveInventoryList(some_object_id, "folder_name", "inventory_list");
call. In that way, most additional Lua functionality will be a fancy “easier” wrapper around functions shared with LSL!
The caveat is if functions are added that use new types that aren't in LSL (like functions that take in a key -> value
map) likely won't be exposed to LSL, as there's no way to represent them in LSL anyway.
Wait, LSL can run on Lua now? How? Why would you do that?
A reasonable question! It sounds crazy at first, but as it turns out, LSL maps really well to Lua bytecode, and we already have a compiler that can output both Mono and legacy LSO2 bytecode. Writing a compiler that could output Lua bytecode wasn't terribly difficult, and it allows LSL scripts to run faster, and be more memory-efficient. Most importantly, it allows us to test behavior of the new scripting engine against the behavior of the existing Mono scripting engine. This helps us make sure we're doing an apples-to-apples comparison on our benchmarks, and that all the nice things that Mono currently gets us will also work correctly under the new scripting engine.
Additionally, for better or for worse, LSL is here to stay. People have been writing scripts in it for 20 years, and they're going to be running on the servers for years to come. Performance and memory usage issues with LSL scripts will continue to matter for the foreseeable future. The faster the server can run existing LSL scripts, the more time it can dedicate to running newer Lua scripts. Memory usage has been a particular problem with the existing Mono scripting engine, and this would help us address this.
What about running scripts on the viewer-side if I want viewer-side effects? Does this have any interaction with the work adding Lua addons into the viewer?
No, the work on adding Lua addons to the viewer is a separate effort. Lua scripts on SL will strictly run on the region, and not on the people's viewers. We're focused on providing a high-quality replacement to the existing server-side scripts before we look at anything else.
Note that we are all too aware of the current difficulty of scripting HUD UIs for Second Life (we make a lot of our own) and we're thinking of ways we can make things better, but there are no definite plans, and it wouldn't involve running scripts on the viewer in the near-term. The current UI tooling exposed to Lua viewer addons isn't tooled for being called from untrusted code either way, so lack of "client-side" scripts is unlikely to hinder any effort to create a better API for HUDs for in-world systems.
You said Lua runs LSL faster than Mono? It uses less memory? How? I'm suspicious.
A reasonable suspicion! We were also suspicious and suspected that Lua would be slightly slower, but in reality, yes, Lua can run LSL scripts quite a bit faster than the custom-patched Mono 2.6 we currently use. The best LSL on Mono has performed compared to Luau in our internal tests is 10% slower than Luau. Similarly, most LSL on Luau scripts end up being around half the size of Mono scripts. That means much more memory to play with in your LSL scripts.
The reason why isn't intuitive, after all, Mono is a lot more intricate and has a lot of fancy performance optimizations like a JIT optimizer, but it becomes clear when you look at the bytecode that's generated for LSL on each scripting engine.
Consider the following LSL function as an example:
float loudSubtract(float val, float sub) {
// subtract, telling the owner what we did
float new_val = val - sub;
llOwnerSay("New val is " + (string)new_val);
return new_val;
}
This compiles to the following Luau bytecode (subject to further optimization):
Lua bytecode |
Function 0 (_floudSubtract): SUB R2 R0 R1 GETIMPORT R3 2 [ll.OwnerSay] GETIMPORT R6 5 [lsl.cast] MOVE R7 R2 LOADN R8 3 CALL R6 2 1 LOADK R5 K6 ['New val is '] CONCAT R4 R5 R6 CALL R3 1 0 RETURN R2 1 |
Okay, that's pretty decent. Some places where things could be more optimized, but no big deal. The equivalent Mono bytecode is (and bear with us, this is long:)
Mono bytecode |
.class public auto ansi serializable beforefieldinit LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame extends [UThread]LindenLab.SecondLife.UThread.UThreadStackFrame { .field public int32 ' pc' .field public class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575 ' instance' .field public float32 val .field public float32 'sub' .field public float32 ' l0' .field public int32 ' l1' .method compilercontrolled specialname rtspecialname instance void .ctor( int32 _param1, class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575 _param2, float32 _param3, float32 _param4, float32 _param5, int32 _param6 ) cil managed { .maxstack 4 IL_0000: ldarg.0 // this IL_0001: call instance void [UThread]LindenLab.SecondLife.UThread.UThreadStackFrame::.ctor() // [29 5 - 29 28] IL_0006: ldarg.0 // this IL_0007: ldarg.s _param1 IL_0009: nop IL_000a: nop IL_000b: nop IL_000c: stfld int32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' pc' // [30 5 - 30 34] IL_0011: ldarg.0 // this IL_0012: ldarg.s _param2 IL_0014: nop IL_0015: nop IL_0016: nop IL_0017: stfld class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' instance' // [31 5 - 31 23] IL_001c: ldarg.0 // this IL_001d: ldarg.s _param3 IL_001f: nop IL_0020: nop IL_0021: nop IL_0022: stfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::val // [32 5 - 32 23] IL_0027: ldarg.0 // this IL_0028: ldarg.s _param4 IL_002a: nop IL_002b: nop IL_002c: nop IL_002d: stfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::'sub' // [33 5 - 33 28] IL_0032: ldarg.0 // this IL_0033: ldarg.s _param5 IL_0035: nop IL_0036: nop IL_0037: nop IL_0038: stfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' l0' // [34 5 - 34 28] IL_003d: ldarg.0 // this IL_003e: ldarg.s _param6 IL_0040: nop IL_0041: nop IL_0042: nop IL_0043: stfld int32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' l1' IL_0048: ret } // end of method LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::.ctor .method public virtual instance object Resume() cil managed { .maxstack 8 // [37 37 - 37 99] IL_0000: ldarg.0 // this IL_0001: ldfld class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' instance' IL_0006: ldarg.0 // this IL_0007: ldfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::val IL_000c: ldarg.0 // this IL_000d: ldfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::'sub' IL_0012: call instance float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575::gloudSubtract(float32, float32) IL_0017: box [mscorlib]System.Single IL_001c: ret } // end of method LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::Resume } // end of class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame // ... Within the actual script class .method public hidebysig instance float32 gloudSubtract( float32 val, float32 'sub' ) cil managed { .maxstack 16 .locals init ( [0] float32 num1, [1] int32 'l1[34-35], num2[69-151], num2[151-152]', [2] class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame c9eec9e37575gloudSubtractFrame, [3] float32 num3 ) // [17 5 - 17 60] IL_0000: call bool [UThread]LindenLab.SecondLife.UThread.UThread::IsRestoring() IL_0005: brfalse IL_0036 // [19 7 - 19 202] IL_000a: call class [UThread]LindenLab.SecondLife.UThread.UThreadStackFrame [UThread]LindenLab.SecondLife.UThread.UThread::Pop() IL_000f: castclass LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame IL_0014: stloc.2 // c9eec9e37575gloudSubtractFrame // [20 7 - 20 53] IL_0015: ldloc.2 // c9eec9e37575gloudSubtractFrame IL_0016: ldfld float32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' l0' IL_001b: stloc.0 // num1 // [21 7 - 21 55] IL_001c: ldloc.2 // c9eec9e37575gloudSubtractFrame IL_001d: ldfld int32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' l1' IL_0022: stloc.1 // l1 // [22 7 - 22 55] IL_0023: ldloc.2 // c9eec9e37575gloudSubtractFrame IL_0024: ldfld int32 LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::' pc' IL_0029: switch (IL_0085, IL_004b) // [31 5 - 31 58] IL_0036: call bool [UThread]LindenLab.SecondLife.UThread.UThread::IsSaveDue() IL_003b: brfalse IL_004b // [33 7 - 33 15] IL_0040: ldc.i4 1 // 0x00000001 IL_0045: stloc.1 // num2 // [34 7 - 34 20] IL_0046: br IL_0087 // [37 5 - 37 70] IL_004b: ldarg.s 'sub' IL_004d: nop IL_004e: nop IL_004f: nop IL_0050: ldarg.s val IL_0052: nop IL_0053: nop IL_0054: nop IL_0055: call float64 [LslUserScript]LindenLab.SecondLife.LslUserScript::Subtract(float64, float64) IL_005a: stloc.0 // num1 // [38 5 - 38 84] IL_005b: ldloc.0 // num1 IL_005c: call string [LslLibrary]LindenLab.SecondLife.LslRunTime::ToString(float32) IL_0061: ldstr "New val is " IL_0066: call string [LslUserScript]LindenLab.SecondLife.LslUserScript::Add(string, string) IL_006b: call void [LslLibrary]LindenLab.SecondLife.Library::llOwnerSay(string) // [39 5 - 39 58] IL_0070: call bool [UThread]LindenLab.SecondLife.UThread.UThread::IsSaveDue() IL_0075: brfalse IL_0085 // [41 7 - 41 15] IL_007a: ldc.i4 0 // 0x00000000 IL_007f: stloc.1 // num2 // [42 7 - 42 20] IL_0080: br IL_0087 // [45 5 - 45 17] IL_0085: ldloc.0 // num1 IL_0086: ret IL_0087: nop // [47 5 - 47 194] IL_0088: ldloc.1 // num2 IL_0089: ldarg.0 // this IL_008a: ldarg val IL_008e: nop IL_008f: nop IL_0090: ldarg 'sub' IL_0094: nop IL_0095: nop IL_0096: ldloc.0 // num1 IL_0097: ldloc.1 // num2 IL_0098: newobj instance void LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575gloudSubtractFrame::.ctor(int32, class LSL_a26e2aa2_c02b_c23e_06be_c9eec9e37575, float32, float32, float32, int32) IL_009d: pop // [49 5 - 49 17] IL_009e: ldloc.3 // num3 IL_009f: ret } // end of method } |
While this might look unreasonably large, this is also pretty similar to how async
C# functions that have await
s get compiled. The code required to allow a function to pause in the middle to let other things run is necessarily very large, and our Mono bytecode instrumentation is essentially a special variant of that which optimizes for cases where the function won't have to pause. Try pasting the following code into sharplab.io (while decompiling to IL) and you'll see something very similar:
using System.Threading.Tasks; public class Example { public async Task llOwnerSay(string val) { // stub } public async Task<float> loudSubtract(float val, float sub) { float new_val = val - sub; await llOwnerSay("New val is " + new_val.ToString()); return new_val; } }
The Lua bytecode ends up being a lot smaller, because the Lua scripting engine itself knows how to handle this case, and doesn't need to add any special code to your scripts to allow them to be paused and resumed in the middle of a function.
Even though Mono has a JIT optimizer, which should in theory make the script run faster, this sort of code is very unusual, and the JIT optimizer isn't able to optimize it very well. That means that even though Luau itself is in theory slower, it's able to run LSL much faster than Mono can.
You might also notice things like call float64 [LslUserScript]LindenLab.SecondLife.LslUserScript::Subtract(float64, float64)
in the Mono code. Those are necessary because of LSL's weird operand evaluation order, but are unnecessary in Luau because of the more efficient form of bytecode that Luau uses.
In general, C# doesn't aim at producing very small bytecode for async functions, which is important for something like Second Life where you might have thousands of different individual scripts running within a region. Luau does generate small bytecode, while still having decent performance, making it a good choice for Second Life. And hey, strings are UTF-8 in Lua instead of UTF-16 as in Mono, so they use much less memory for most data as well.
Are we going to get a higher memory limit in scripts?
Not at first, Luau uses less memory than Mono, so you shouldn't really miss it. You're not going to start hitting script memory limits just because you converted your script to Lua. Using Lua proper should allow you to more precisely control memory usage in your scripts, e.g. by being able to handle memory allocation errors in pcall()
and access to the mutable buffer
class for byte-packing.
There is some interest in giving specific scripts a little more memory if they're designated "high priority", like a main "game manager" object for an experience, but there are no specific plans for this at the moment.
Are my existing LSL scripts going to run on Lua Automatically?
While we can run LSL in Lua now, and the idea is to eventually phase out Mono, there’s no immediate plans to transparently migrate existing scripts over to Lua. There are ideas about how this could be done safely without breaking existing content, but we want to make sure we have everything right before we start work on this.
How does interoperation with LSL work?
Everything that you expect from LSL should have some equivalent in Lua. There’s key
s, rotation
s and vector
s and something like some_vec * some_rot
in LSL is the exact same in Lua scripts.
All LSL functions will be available in Lua, and something like llSay(0, “hi!”)
is just ll.Say(0, “hi!”)
now. What about llSetPos(llGetPos() + <0, 0, 1>);
? Just ll.SetPos(ll.GetPos() + vector(0, 0, 1))
.
No special magic required here, and you can write whatever fancy wrappers you want around the existing LSL functions. We’ll be looking at how residents actually write these wrappers to inform the design of “official” higher-level Lua APIs, so we’re excited to see your ideas!
Can I mix LSL and Lua in the same script?
No, although most things should be a direct translation. At some point we may make a tool to help you do this, but there are no specific plans at the moment.
If your object has multiple scripts, you can mix LSL and Lua within the same object, though.
Can we use all of Lua? Is anything not there?
Most everything will be there, except for two notable exceptions:
getfenv()
/setfenv()
/_ENV
are not accessible because they necessarily make the script harder to optimize. Usually people only use this for mocking in test code, and this can be accomplished by passing in a parameter with a table of function references rather than pretending you have different global variable values for certain functions.loadstring()
is not supported since it has a huge impact on performance. For those not familiar, this is equivalent to eval() in other languages. Every single time you callloadstring()
the compiler has to go over what you passed in, compile it, and then load the generated bytecode. The Lua compiler isn’t particularly fast, and doing this can cause your script to halt in a way that we can’t interrupt to give other scripts time to run. This is a big part of whyeval()
andloadstring()
are generally considered poor programming practice.
The one real use-case we could think of for loadstring()
is a debugger, and we’re looking at implementing a “real” debugger that lets you step through your code. Note that this won’t happen with the initial release, but watch this space!
One thing that is planned, but will likely not be in the initial release is the coroutine library. This is one we’d really like to have in there, so you can do fancy things like have multiple “timers” in a script by just having coroutines that sleep every so often, but multiple running coroutines within the same script doesn’t fit well with the existing scripting architecture the Second Life server uses. Rest assured, this will be in there soon™ after Lua is released, but in the meantime you can still use something like the Promise
chaining that JavaScript uses to write asynchronous event handlers.
We’ll have code demonstrating "proper" usage of Lua around the time that the public beta starts.