First and foremost before we get started please remember– this is not the official style guide of Lua or Luau.
The purpose of following a style guide is to keep consistent and clean code. You may choose to follow or take inspiration or if you're not here to read about style practices, you may simply read this for enlightenment. This guide assumes you have a general understanding of Luau. This guide also goes into depth into why you should follow such practices. The goal of this style guide is to promote styles that respect the modified LSP of Lua 5.1 external Luau-like analysis that are also in regards to developer ergonomics. This guide values styles that promote readability and in a language that is being constantly optimized– the era of focusing on performance over readability is slowly coming to an end.
This guide at the moment does not support static type checking.
Single line comments are to have spaces after the double hyphen like so:
Block/body comments are supposed to be formatted like so:
Comments should be kept minimum, provide comments only when needed. For example describing a series of semantics used to achieve something and when your code doesn’t express those semantics clearly is a good example when to provide comments. Otherwise you shouldn’t. Providing more comments makes code much more harder to read. Though this seems like a very contradicting statement– it’s true. What takes more longer to read, a 5 paragraph essay or a 1 paragraph essay? Many famous authors like Robert C. Martin have mentioned this. In Short, if you find yourself practicing amplification on comments, you’re doing something wrong. [3]
Code should be self-documenting, however, open-source APIs follow different rules. You should be documenting your APIs well with comments to further abstract your code. The user shouldn’t have to read your API code to figure out how to use it.
Comments should always be placed inside scopes. Most code editors and IDEs will close out scopes and you can use this to your advantage to hide comments.
Always scope your variables locally. Not only are the usage of global variables in Luau very slow but much more confusing. There are times where you can get reassignment confused with the declaration of a global. You’ll also end up using more resources and because of this, you’ll have to come up with silly variable names like: Jump2 or NewJump because the previous variables couldn’t be garbage collected. In short, there is never a real reason to have a global– avoid this at all times.
Unlike globals, local variables don't require any special instructions. From an internal-view– Luau (at the time this guide was written) generates the same instructions as native Lua 5.1, SETGLOBAL for defining a global and GETGLOBAL for fetching a global. Globals are stored on the environment and fetching these takes much longer. Globals are fetched with a dictionary look-up on the environment while locals only require to be indexed from an array stored on the stack in some register. Globals are also never garbage collected. This also at the time of this writing conflict with parallel Luau as the environment is only isolated to the VM it's running in, therefore this will break dependencies unless Luau adds some secure shared storage between multiple VMs that still follow the principles of VM isolation for Luau sandboxing.
This style guide unlike most guidelines doesn't follow multiple casing. You should use only one casing style– preferably PascalCase– and for everything. The idea of having specific casings for specific variables is to denote a certain variable type (like an argument), however, this has problems.
The problem with different casing patterns:
A simple solution is naming your variables correctly in the first place– give proper meaning to your variables instead of relying on specific casings for variables. Using camelCase in Luau is useless and I find this especially more inconsistent (no disrespect to those who use this casing). The casing is understandable for object-oriented programming in other languages but Luau doesn't implement built-in classes or an inheritance system (at the time this guide was written).
The arrangement not only follows a near universal standard of variable arrangement but goes into depth promoting developer ergonomic benefits to help you write better and more readable code.
Ideally, you should always require modules first since standardly in most programming languages you always import/load/require modules first.
After modules in a new section, you fetch services. Sometimes services may have to be declared before modules. Always use GetService().
After services in a new section, you fetch and store assets using WaitForChild.
After assets in a new section, you store constants and setting variables that are meant to modify your script
After settings in a new section you store variables that your code will use and modify at run-time.
Tables are meant to go last, after regular variables– all in the same section or if needed, a new section
After your variables you have your functions; arranged from auxiliary functions that are least commonly called to most commonly. Right after your auxiliary should come your main functions.
Here is an example of how variables should be formatted (Commenting out sections are optional, I did this for a clearer visual representation):
Vertical alignment and both regular alignments are completely fine. Personally I find vertical alignment more pleasing to read, especially when you have a lot of defined variables. Most IDEs have shortcuts to automatically vertically align your code and this style is totally acceptable by Luau’s flexible lexer and linter. Although you will have to stick to one alignment style for consistency.
When assessing variables you should do this in an idiomatic matter. Use the not operator instead of ~= nil/ ~= false. Only use ~= nil when you strictly have to check if the variable is nil. Do if Exist then instead of if Exist == true then.
Good:
Bad:
When necessary:
Internally, the idiomatic convention generates a different bytecode compared to the strict imperative evaluation: Luau (at the time of this writing) still uses native Lua 5.1’s instruction for handling these types of semantics. The instruction: TEST for the mentioned assessment which proves to be faster than the instruction for the non-idiomatic evaluation: EQ. Lua’s VM will execute the TEST instruction in conjunction with the JMP instruction to implement short-circuit LISP-style logical operators that retain and propagate operand values instead of booleans. Though the performance differences between the latter is quite miniscule, this settles arguments for those who argue over what’s the faster method.
Source: https://www.lua.org/source/5.1/lopcodes.c.html
Source: https://www.lua.org/source/5.1/lvm.c.html
This style guide follows the standard convention for dummy variables like so:
Stay consistent with syntax and follow the rules!
When declaring a string literal, use double quotes: " "
For consistency reasons, you should always use the syntax: Call(…) to call a function no matter the micro-performance boost. In some cases for readability purposes it is reasonable to use the syntax: Call{…}. Here's a good example of implementing a table call expression:
Unfortunately, the only exception besides the default call-statement was the table-call– this means that the string-call expression is not favored by the style guide. For one, the syntax is not appealing cognitively and two, not many people use the syntax which brings inconsistencies.
The aforementioned micro-performance benefits are only the result of a more compact AST production. The bytecode generated will be the same however the tokens are being assembled more differently into an abstract syntax tree. The difference is that the argument node doesn’t build sub-nodes for more arguments– instead it re-uses the same argument node to place the literal argument set. Remember native Lua does not have a AST generation phase as it uses a one-pass compiler in contrast to Luau, however, there are AST inspection tools that you can use. It seems this optimization is applied by those ASTs inspectors commonly so it could be something that Luau would apply which would explain for the almost unnoticeable speed differences. Here is the AST format below:
Generates the following AST (https://astexplorer.net/):
The benefit is that during the sementical phase of compilation, we only traverse less nodes.
You should be indenting code standardly or stylistically (which will be discussed later on) for each scope. Leave whitespaces to section off code. Here’s a good example of separating stuff with whitespacing:
Don’t use semicolons for every statement to show a line has ended– the lexer is flexible enough to generate tokens and lexemes correctly. Only use semicolons when you have multiple statements in a single line. Here’s an example:
The style guideline does promote the use of multiple statements in a single line but only to a certain extent. It's a good way to group code into sections and visually reduce your LOC.
When you have more complex expressions or statements– the ones that especially span over lines are the ones where you should optionally use semicolons like so:
Expressions must be constructed in single statements like so:
Paranthesis shouldn't be used in if-statements unless you have ternary operators which will be discussed later on.
The shown pattern above is interpreted by many as a code smell. The modified Lua 5.1 language server for external Luau-like analysis in IDEs won’t function properly with this. This code smell also exists in other languages and IDEs like JetBrains will flag this. Remember this style guide respects Luau’s linter analysis’s rules and all mentioned styles are in regard to proper linter functionality.
Conditional branching is the biggest way to reduce readability besides bad variable naming– this means that you should keep branching to a minimum when possible to lower the cognitive complexity of your code [2]. If needed you may take the inverse of your condition to optimize the cognitive complexity like so:
This can be optimized by preserving the semantics and only taking the inverse of a few conditions to generate less branching and LOC. In many times, we can use this to optimize the control-flow graph and produce less edge cases and inherently reduce quantitative metrics measuring the complexity of our code like so:
In-line conditionals are completely allowed and encouraged as again our flexible lexer will generate tokens just fine along with regards to static analysis. A general rule is that in-line if-statements should be avoided if they become too long. This is determined intuitively by the programmer.
Short-circuit evaluation also known as “minimal evaluation”, is another practice used to create more readable code– this will not raise the cognitive complexity of your code:
Cognitive Complexity also ignores the null-coalescing operators found in many languages,
again because they allow short-handing multiple lines of code into one. For example, both of
the following code samples do the same thing:
The meaning of the version on the left takes a moment to process, while the version on the
right is immediately clear once you understand the null-coalescing syntax. For that reason,
Cognitive Complexity ignores null-coalescing operators.
G. Ann Campbell. 2018. Cognitive Complexity - A new way of measuring understandability. Technical Report. SonarSource SA, Switzerland. https://www.sonarsource.com/docs/CognitiveComplexity.pdf
Ternary operators can become harder to read which is why it is important to provide parenthesis like so:
Bad:
Parentheses should only be spaced in between if your parentheses are nested like so:
Table elements must be separated only by commas. Semicolons are discouraged and bad practice as they are used instead to show a line has ended. This may also cause confusion in table arrangement so it’s best to steer away and stick to consistent code.
Linear Arrays that hold only indices must be structured like so:
If your Array holds too many indices to be grouped into a single line– you should make them multi-lined like so:
Dictionary-tables are structured like so and will always be multi-lined:
If you find yourself implementing data structures in tables you must have names referring to their abstract data types somewhere in the name of your table variable, like so:
When you construct a table and later populate you should comment future elements like so:
When possible you should always be indexing your tables with the following notation:
Over
As of the writing of this style guide, Luau only does inline caching for the first shown example. Luau implements this using a mechanism that is used by many VMs like JVM and LuaJIT. The compiler can predict the hash slot used by field lookup, and the VM can correct this prediction dynamically [1].
If your Tables take up too much space– it is best to abstract them into modules which should preferably be placed underneath your script or module. Less code is always better code.
Your functions should be structured consistently like so:
Example:
When you reach over 5 arguments to your functions you should use tables instead. As your parameters grow it increases the chances of parameter positional mismatch which is why you should take the aforementioned developer ergonomic approach. You’ll also actually save VM registers.
Function parameters should be commented out with their types like so (if you don't use Luau's static type checking system):
Exactly as formatted above.
You should always be doing proper error handling to keep efficient maintainability of your code.
This guide is against the assert function. Simple short circuit evaluations can be used which look much more readable like so:
Internally, assert handles errors much efficiently but we care about readability more and worrying about how efficiently code is going to error is completely extraneous as our code is going to stop operating anyways.
Sometimes we want a more efficient and readable way to handle erroring and safety checks. We can apply the concept of unit-testing and create readable tests like below:
Although preferably when we are doing tight performance operations– unit-test patterns can become a problem however Luau is constantly optimizing.
type() should only be used to check native types for consistency and context reasons while typeof() should only be preserved for Luau types.
Metadata should always be abstracted into modules.
Your Metadata module should return an anonymous table unless needed. Here's an example below:
Metatables should only be used for object-oriented patterns. Internally, Lua’s meta-mechanisms are very inefficient as numerous register accesses have to be made and elements have to be copied and moved continuously. Do not worry, however, as Luau plans on implementing a built-in inheritance syntax in the near future at the time of this writing.
Your index metamethod should point directly back at the table for Luau optimizations to be made and for fast method calls to be properly made [1]. Classes should be formatted just like below:
You’re constructor must be constructed and accessed using the following notation below:
Your constructor may be PascalCase or camelCase, this one is up to you as people commonly use camelCase for their constructors. Methods must use the notation: Object:Hello()
You should denote private fields with the convention: _ before the variable or function like so:
Here is an example of how everything should go together:
[1] https://roblox.github.io/luau/performance
[2] G. Ann Campbell. 2018. Cognitive Complexity - A new way of measuring understandability. Technical Report. SonarSource SA, Switzerland. https://www.sonarsource.com/docs/CognitiveComplexity.pdf
[3] Martin, Robert C. Clean Code: A Handbook of Agile Software Craftsmanship. Upper Saddle River, NJ: Prentice Hall, 2009. Print.
Luau
, Lua
, StyleGuide