Acquis 28 - Flags
Contents (7)
The flags table is a special readonly table populated from the command line. Lookups on it inside if conditions are evaluated at compile time when possible, and the losing branch is removed from the generated code entirely.
Defining flags#
The --flag/-F command-line argument stores a value in the table. The value defaults to true, but can also be a number or a string depending on the assignment syntax:
lus -Fdebug script.lus # flags.debug == true
lus -Fversion=1.5 script.lus # flags.version == 1.5 (number)
lus -Fname=alpha script.lus # flags.name == "alpha" (string)
lus --flag channel=beta script.lus # same as -F
The table always exists, even when no flags are given; flags that were never defined read as nil.
-- Run with: lus -Fdebug -Fversion=1.5 script.lus
assert(flags.debug == true)
assert(flags.version == 1.5)
assert(flags.absent == nil)
Compile-time folding#
The parser detects flags lookups in if/elseif conditions and computes the branch ahead of time. When the condition resolves truthy, the body is kept and the rest of the chain is discarded; when it resolves falsy, the body is discarded and the chain continues. Both the bare and negated forms fold:
-- With -Fprerelease, this body is compiled; without it, the body is
-- removed from the generated code entirely.
if flags.prerelease then
print("/!\\ THIS IS A PRE-RELEASE VERSION OF THE PROGRAM")
end
if not flags.quiet then
print("starting up")
end
Discarded branches are still fully parsed, so syntax errors surface no matter which flags are set. Only the code generation is skipped.
Conditions the parser cannot resolve (any shape other than flags.NAME or not flags.NAME, such as comparisons, parenthesized lookups, or and/or chains) are compiled normally and read the table at run time, producing the same result:
if flags.name == "alpha" then -- evaluated at run time
print("alpha channel")
end
Reserved name#
flags is a reserved name, usable without a global declaration. Assigning to it is a compile-time error, declaring it is rejected, and writing fields fails at run time. Locals may still shadow it:
local f1 = load("flags = 1")
assert(f1 == nil) -- attempt to assign to const variable 'flags'
local f2 = load("global flags")
assert(f2 == nil) -- cannot declare 'flags' (reserved name)
local ok = catch do flags.x = 1 end
assert(not ok) -- attempt to modify a readonly table
local flags = {debug = "shadowed"}
assert(flags.debug == "shadowed") -- locals shadow the reserved name
Interaction with other features#
- Workers see the same flags as the parent interpreter.
- Chunks loaded at run time with
loadfold against the same table. - Standalone executables built with
--standalonepreserve their-Fflags, so the bundled program folds and reads the same values at run time. lusccompiles without any flag context: nothing folds, andflagslookups compile to ordinary reads resolved by whichever interpreter runs the chunk.- Flag values are fixed for the lifetime of the process. Bytecode produced by
string.dumpbakes in the folding decisions made at compile time.
Motivation#
Build-variant switches (debug logging, feature gates, pre-release banners) are usually implemented with globals checked at run time, paying for the check on every execution and shipping dead code for every variant.
Zero-cost configuration#
Because the winning branch is selected during parsing, a disabled if flags.debug then ... end block costs nothing at run time: no lookup, no test, no dead code in the generated bytecode. The program shrinks to exactly the variant selected on the command line.
Safety#
The table is readonly and its name reserved, so the configuration a program was launched with cannot be tampered with from inside the program: folded and non-folded reads always agree.