5
votes

I'm lead dev for Bitfighter, and we're using Lua as a scripting language to allow players to program their own custom robot ships.

In Lua, you need not declare variables, and all variables default to global scope, unless declared otherwise. This leads to some problems. Take the following snippet, for example:

loc = bot:getLoc()
items = bot:findItems(ShipType)     -- Find a Ship

minDist = 999999
found = false

for indx, item in ipairs(items) do           
   local d = loc:distSquared(item:getLoc())  

   if(d < minDist) then
      closestItem = item
      minDist = d
   end
end

if(closestItem != nil) then 
   firingAngle = getFiringSolution(closestItem) 
end

In this snippet, if findItems() returns no candidates, then closestItem will still refer to whatever ship it found the last time around, and in the intervening time, that ship could have been killed. If the ship is killed, it no longer exists, and getFiringSolution() will fail.

Did you spot the problem? Well, neither will my users. It's subtle, but with dramatic effect.

One solution would be to require that all variables be declared, and for all variables to default to local scope. While that change would not make it impossible for programmers to refer to objects that no longer exist, it would make it more difficult to do so inadvertently.

Is there any way to tell Lua to default all vars to local scope, and/or to require that they be declared? I know some other languages (e.g. Perl) have this option available.

Thanks!


Lots of good answers here, thanks!

I've decided to go with a slightly modified version of the Lua 'strict' module. This seems to get me where I want to go, and I'll hack it a little to improve the messages and make them more appropriate for my particular context.

4
BTW, do you have any good reason to put extra braces inside your if statements? - Alexander Gladysh
Well, before you posed the question I would have answered that they were required. But now I have to answer that omitting the parens just looks wrong to me! - Watusimoto
It is a personal preference of course. But it is important to remember that Lua is not C or Pascal or whatever. Lua is Lua and to use it effectively you must use it as it is, not as if it is some poor substitute for other programming language. I've found that such "syntax looks wrong" things do help to put Lua into part of my brain distinct from C++ and other languages I know. In short, my opinion is: if it is written in Lua, write it in Lua way! :-) - Alexander Gladysh
Yes, you make some good points. Lua is not C, but there are certain C conventions that I think make code much more readable (for me, at least :-). I have conquered my earlier habit of ending lines with a semicolon, and I do try to make my formatting style a little different when I'm working with C++ and Lua. But I'll probably be sticking with the parens in a conditional clause for a while longer. - Watusimoto
So Alex, if I understand you, if Lua allows multiple ways to write something, the ways that do not resemble other language forms are to be preferred over the ways that do resemble other language forms? If this is so, then we must strive to write Lua code that is opaque as possible to non-Lua coders. - Stomp

4 Answers

3
votes

There is no option to set this behavior, but there is a module 'strict' provided with the standard installation, which does exactly that (by modifying the meta-tables). Usage: require 'strict'

For more in-depth info and other solutions: http://lua-users.org/wiki/DetectingUndefinedVariables, but I recommend 'strict'.

2
votes

Sorta.

In Lua, globals notionally live in the globals table _G (the reality is a bit more complex, but from the Lua side there's no way to tell AFAIK). As with all other Lua tables, it's possible to attach a __newindex metatable to _G that controls how variables are added to it. Let this __newindex handler do whatever you want to do when someone creates a global: throw an error, permit it but print a warning, etc.

To meddle with _G, it's simplest and cleanest to use setfenv. See the documentation.

2
votes

"Local by default is wrong". Please see

http://lua-users.org/wiki/LocalByDefault

http://lua-users.org/wiki/LuaScopingDiscussion

You need to use some kind of global environment protection. There are some static tools to do that (not too mature), but the most common solution is to use runtime protection, based on __index and __newindex in _G's metatable.

Shameles plug: this page may also be useful:

http://code.google.com/p/lua-alchemy/wiki/LuaGlobalEnvironmentProtection

Note that while it discusses Lua embedded into swf, the described technique (see sources) do work for generic Lua. We use something along these lines in our production code at work.

0
votes

Actually, the extra global variable with the stale reference to the ship will be sufficient to keep the GC from discarding the object. So it could be detected at run time by noticing that the ship is now "dead" and refusing to do anything with it. It still isn't the right ship, but at least you don't crash.

One thing you can do is to keep user scripts in a sandbox, probably a sandbox per script. With the right manipulation of either the sandbox's environment table or its metatable, you can arrange to discard all or most global variables from the sandbox before (or just after) calling the user's code.

Cleaning up the sandbox after calls would have the advantage of discarding extra references to things that shouldn't hang around. This could be done by keeping a whitelist of fields that are allowed to remain in the environment, and deleting all the rest.

For example, the following implements a sandboxed call to a user-supplied function with an environment containing only white-listed names behind a fresh scratch table supplied for each call.

-- table of globals that will available to user scripts
local user_G = {
        print=_G.print,
        math=_G.math,
        -- ...
    }
-- metatable for user sandbox
local env_mt = { __index=user_G }


-- call the function in a sandbox with an environment in which new global 
-- variables can be created and modified but they will be discarded when the 
-- user code completes.
function doUserCode(user_code, ...)
    local env = setmetatable({}, env_mt) -- create a fresh user environment with RO globals
    setfenv(user_code, env)        -- hang it on the user code
    local results = {pcall(user_code, ...)}
    setfenv(user_code,{})
    return unpack(results)
end

This could be extended to make the global table read-only by pushing it back behind one more metatable access if you wanted.

Note that a complete sandbox solution would also consider what to do about user code that accidentally (or maliciously) executes an infinite (or merely very long) loop or other operation. General solutions for this are an occasional topic of discussion on the Lua list, but good solutions are difficult.