strict.lua, for Lua modders with big thumbs

Pazyryk

Deity
Joined
Jun 13, 2008
Messages
3,584
...or really for any modder who has ever made a typo and has >100 lines of Lua code.

Download Here
Current version 0.10

Consider this code:
Code:
local bWrapX = Map.IsWrapX()
function AdjacentPlotIterator(plot)
	[COLOR="Silver"]< snip >[/COLOR]
	if bXWrap then
		[COLOR="Silver"]< snip >[/COLOR]
	end
end
It causes no error at all. It simply doesn't work as intended. The misspelled bXWrap is interpreted as a non-declared global which evaluates to nil, equivalent to false in this context. This bug was present in Éa for 2 yrs unnoticed! :mad: Well, it's an edge-case (literally) and I guess no players had attempted an area-effect spell at the map edge. Or more likely, some players did and were annoyed but never reported the bug.

In many cases where a typo doesn't cause an immediate error, the reason is that Lua treats the misspelled identifier as a new global that hasn't been assigned to yet. Normal Lua behavior when encountering a nonexistent global is to evaluate it as nil (if an access attempt) or to create a new global variable (if an assignment attempt). Either can change code logic in some very bad way but not cause an explicit error, therefore going unnoticed or leading to serious modder confusion and frustration ("I'm sure I added map edge logic!").

With strict.lua, you can identify unintended accesses or assignments to nonexistent globals. And all of this can be done for table keys too! (Although tables need to be made strict explicitly.)

Perhaps I have bigger thumbs than most modders, but in Éa this has identified well over 50 code mistakes, maybe 20 of which had real or potential bad impacts, the remainder being mostly cases of global variables that should have been locals. All fixed now! :D

How to use: the short version

Add strict.lua to your mod, set VFS = true, then add the statement...
Code:
include("strict.lua")
...early in your mod's loading code. You also need to enable debug library if it isn't already. To do this, go into your config.ini file and make sure this setting is 1:
Code:
; Enables the use of the standard Lua debug library.
EnableLuaDebugLibrary = 1
Now, run your mod as you normally would. Nothing has changed! The only thing strict.lua does (with default settings) is to print problems that it sees. Specifically (with default settings) it sees assignment to nonexistent globals within functions and access to nonexistent globals anywhere as problems. Maybe you intended to do this, but strict.lua thinks it's probably a typo.

Many access/assignment errors may be identified immediately when your mod loads, while others will be identified only when particular functions runs. Long autoplay sessions is a great way to test out all your functions, assuming they run at some point. You can then search the Lua.log for strict.lua errors, or (if Civ5 is still running) use PrintStrictLuaErrors() to print all accumulated non-redundant errors. By default, a particular error at a particular place (file and line number in your code) will print only once.

Of course, you could just look through errors and ignore those that really aren't. A better approach is to use the included function Globals("global1", "global2", "global3", ...) so that these are ignored by strict.lua. An even better approach is to do some code organization and adhere to one basic rule so that you can easily spot future errors with strict.lua. With default settings, the only rule is that globals have to be assigned something (even nil works) in the main body of your mod (i.e., not inside a function) before they are assigned to in a function or accessed anywhere. "Main body" could be in an included file, so you could have a separate "Declarations.lua" file where you do all your global declarations.

Thus, if you have a global called bModInited that gets set to true in a function (but was never declared false in the main body), strict.lua will see that as a violation and complain. To make it not a violation, all you have to do is assign it bModInited = false somewhere in your main body or use Globals("bModInited") (although I'd recommend the former). Then strict.lua will tell you if you ever write a code line like: "if bMOdINited then".

The same functionally can be applied to selected tables if you wish, so that misspelled keys won't mess you up. See details for this below.

Good luck slaying typos! Please report back how many coding mistakes/typos you find, and how many with functional consequence.

How to use: the long version

Just a note on the debug library first. Your mod users probably don't have it enabled. But you don't have to change anything! The functions included with strict.lua will simply do nothing (safely) in this case. Therefore, you don't need to change anything for development versus production code.

If you leave bAssert = false (which I recommend), then strict.lua isn't strict, strictly speaking. There is no enforcement of rules and your code runs exactly as it normally would. The only effect is that violations of strict.lua are printed as they occur. For convenience, non-redundant errors are remembered and can be printed all at once using PrintStrictLuaErrors().

Include strict.lua in your mod as stated above. Before this line, there is of course no effect and functions (below) are not available. After this line, strict.lua is in effect and violations are identified and printed. If you want strict.lua to apply to different Lua states, just include it from each state. (Note: the last statement applies to globals which are specific for each Lua state. Tables don't have a state so a strict table acts that way no matter what state or mod is accessing/assigning to it.)

If you want table keys to act in a strict way, you have to make the table "strict" explicitly using MakeTableStrict(table). See Strict Tables section below for details.

OK, but where/how do I declare globals now so that only typos/mistakes generate strict.lua violations?

There are really two "modes" for using strict.lua depending on whether you leave bTestMainBody = false [default] or change it to true. With bTestMainBody = false, you probably don't need to do anything. If you've organized your code in a sensible way, then most strict.lua violations are probably real typos, logical mistakes or (at best) globals that should be locals. Setting bTestMainBody = true will certainly identify more mistakes/typos, but may generate a lot of violations depending on how your code is organized. These two modes are covered in the next sections.

bTestMainBody = false (as shipped)
Basically as described in the "How to Use: Short Version" above. In this mode, globals can be declared anywhere in the "main body" of your code -- that is, anywhere not within functions. You can declare a global by assigning anything, even nil. (But keep in mind that "global = nil" only works to declare a global after strict.lua is loaded.) Assigning to a nonexistent global within a function is always a violation of strict.lua. Accessing a nonexistent global is a violation of strict.lua whether it occurs in the main body or in a function.

bTestMainBody = true (requires organization of global declarations, but I recommend it!)
This will allow detection of accidental global assignments from misspelling anywhere. In this mode, all accesses and assignments to globals are violations, even in main body, except those specifically allowed by:
  • Anything assigned a non-nil value before including strict.lua.
  • Anything declared by Globals("global1", "global2", ...) after including strict.lua.
  • Functions in the main body if you leave bIgnoreFunctionsInMainBody = true [default]. If you didn't know it, all of your function definitions are just assignments of values (of type function) to ordinary Lua variables, which can be local or global. So I allow global function declaration in main body by default. A more strict coding organization would be to set this to false, then declare all your functions first (as you would have to in C++) and then define them in the main body.
  • Tables in the main body if you set bIgnoreTablesInMainBody = true [default = false]. This is here for Object Oriented-style programming where everything is done in global tables. To be very strict, however, you would leave this false and declare all those global tables first before you assign to them in the main body.
You basically have two ways to organize your code in this mode:
  1. assign all globals (except ignored types) with some non-nil value before you include strict.lua (recommended), or
  2. include strict.lua and then use its Globals function to declare all globals.
I'd say #1 is probably better, since it's regular Lua and you can assign actual values at the same time you declare (but they all have to be non-nil if assigned before including strict.lua!). Declaring with Globals looks a little clunky because you have to use strings as args, and you can't assign a value at the same time. What I do in Éa is to have include("EaDeclarations.lua") immediately followed by include("strict.lua"). The only thing I use Globals for is to declare 4 globals that are used by whoward69's river connection code which is used by Éa. (I didn't have to work hard to find these: strict.lua found them for me.) So that's a way to deal with code included from other sources that you don't want to modify.

Remember! If you are assigning to globals before including strict.lua, they have to be assigned some non-nil value. That could be false, 0, "", {}, function() end, or anything else you want. If you want the "most strict" settings with bIgnoreFunctionsInMainBody = false, then I suggest something like this in your declarations file:
Spoiler :
Code:
NULL_FUNCTION = function() end
MyFunction1 = NULL_FUNCTION
MyFunction2 = NULL_FUNCTION
MyFunction3 = NULL_FUNCTION
MyFunction4 = NULL_FUNCTION

--Of course, Lua doesn't mind type-swapping, so you could just as well assign
--them false. But the code above is aesthetically more pleasing.
Globals("global1", ''global2", "global3", ...)
Regardless of settings above, you can "declare" globals at any time in any part of your code with Globals. Basically what this is doing is exempting specific global names from strict.lua scrutiny. Global names have to be in quotes! You can add any number of args and add them whenever, wherever you want (not necessarily in one place).

Strict Tables too!
You can add strict behavior to selected tables so that keys must be declared before they are accessed or assigned to. It's really the same exact problem as globals: mistyping a key gets you nil or creates and assigns to a new key when you intended to assign to an existing key. You probably don't want strict behavior in all of your tables. But it is particularly useful for tables with string keys that are manually typed in your code. Not only will you find typos, you may find (as I did) cases of spelling the key correctly but using it in the wrong table.

If you want to make a table strict, you need to create the table first and assign all keys you will ever use with some non-nil value. Then use MakeTableStrict(table). Remember that you can never add another key, so table.insert is now off-limits! Any subsequent reference to a nonexistent key, whether access or assignment, is a strict.lua violation.

So, for example,
Code:
if table.key then
is a violation if the key doesn't exist. But that's what false is for, so use it.

Of course, there are very sensible reasons why you might want to do nil tests on particular tables. For example, you have a list of things added to by code and you want to check if some key is in the list. But these are exactly the tables that should not be strict! This is why you need to decide for yourself if a table should be strict.

Some technical notes:
  1. You can persist strict tables with TableSaverLoader just fine. However, they won't be strict anymore when loaded. So use MakeTableStrict for a table after initing it with all its keys (for a new game) or after loading it (for a loaded game).
  2. If you have set a metatable for a table, then MakeTableStrict(table) will print an objection and refuse to do anything with it. If you don't know what a metatable is, then you don't need to worry about this. If you do know how to add metatables, then you know enough to modify strict.lua to allow for this to work if you want.
  3. Tables don't exist in a particular "place" (i.e., in one Lua "state" or even in one mod). They are easily available and can be accessed (shared) by different states or mods. If you make a table strict then it will act that way no matter where the strict.lua violation occurs.

Full list of functions
  • Globals("global1", "global2", "global3", ...) Any number of string labels can be added with this function. It's cummulative so additional labels can be added anywhere at any time. These labels will be completely ignored by strict.lua for both access and assignment as globals.
  • MakeTableStrict(table) Make a table act in a strict way so that access or assignment to nonexistent keys is a violation.
  • PrintStrictLuaErrors() Prints a non-redundant list of all strict.lua access and assignment violations.
  • PrintGlobals() Try it in Fire Tuner!
Note: All of these functions are entirely harmless if the debug library is not enabled. They just won't do anything (harmlessly) so cost nothing to leave in your code.

strict.lua adds NO overhead!

Well, except when printing violations. It works by setting metatables for selected tables (the "environment" being the table for globals). The important point is that metatables are only checked by Lua when attempting to access or assign to a nonexistent key (or access or assign to a nonexistent global, same thing). That only happens when you are declaring a new table key (or global, same thing) or causing a strict.lua violation. Otherwise, all accesses and assignments are happening via normal Lua and strict.lua has no bearing on the operation at all.
 
Top Bottom