# `nupp.log` `nupp.log` is leveled logging whose disabled path is the one it is designed around. A severity call in statement position is lowered rather than called, so a filtered line evaluates none of its arguments. ```nupp nupp.log.error("cannot open %s: %s", path, reason) nupp.log.warn("retrying in %dms", delay) nupp.log.info("loaded %d entities", count) nupp.log.debug("state=%s", state) ``` Each severity accepts a `string.format` directive string and the arguments it calls for. The directives and their argument types are checked where the call is written, by the same machinery that checks `string.format`, so a missing argument or a `%d` handed a string is a compile error rather than a line that fails at run time. ```text nupp.log.error("id %d") error: NUPP2006: omitted argument 2 supplies nil, not number ``` Because `%s` accepts anything, nothing needs `tostring` and a nil argument prints as `nil` rather than raising. Lines go to standard error until a host says otherwise. `%?` is Nupp's debug directive. It requires `nupp.Debug`, calls the value's `debug()` method, and formats the returned string as `%s`: ```nupp nupp.log.debug("state=%?", state) ``` At lowered call sites that method call is inside the level guard, so filtered logs do not render the value. Named loggers perform the same check in their already-lazy writer. ## Severity calls are intrinsics A severity call in statement position whose format is a literal is lowered rather than called: ```nupp nupp.log.error("id %d", id) ``` ```lua const __nuppModule = _G.nupp.log.forModule("amb"); -- once, in the prologue if __nuppModule.on[1] then __nuppModule.emit(1,47,string.format("id %d",id)) end ``` Three properties follow from that shape, and together they are the reason this is a compiler intrinsic rather than a library. ### Filtered calls evaluate nothing The level test stands at the call site, so the arguments of a suppressed line are never computed. `nupp.log.debug("%s", render(state))` does not call `render` when debug is off. No library can decline to evaluate its own arguments. ### Module name and line are constants The compiler is generating the file, so it writes both in directly: the module once in the prologue, the line at each site. Nothing is recovered at run time, nothing depends on the `debug` library, and there is no per-module boilerplate to write or to keep in step with a rename. ### Filtered call cost A filtered call costs an upvalue read, an array index and a branch. The view is bound once per module, and `on` is an array indexed by severity, shared with every other module so a level change is seen everywhere at once. ### Forms that stay calls Every other form keeps an ordinary call meaning exactly the same thing, only slower. The module shows as `?`, and the arguments are evaluated: | Form | Lowered | Reason | | --- | --- | --- | | `nupp.log.info("id %d", id)` | yes | | | `nupp.log.info(format, id)` | no | the format is not a literal | | `local f = nupp.log.info` | no | a value, not a call | | `x = nupp.log.info("hi")` | no | not statement position | | `local nupp = ...` | no | not the compiler-provided `nupp` | | `logger:info("id %d", id)` | no | a named logger, not the path | ## Levels `level` reads the threshold, and moves it when given one: ```nupp nupp.log.level("debug") -- set, answers the previous one nupp.log.level() -- read ``` The five levels are `"off"`, `"error"`, `"warn"`, `"info"` and `"debug"`, each admitting itself and everything above it, so `"warn"` emits warnings and errors. The default is `"warn"`. The parameter is that literal union, so a string from outside the program has to be narrowed to one of the five before it can be passed. `os.getenv("LOG_LEVEL") or "warn"` is `string`, and `string` is not one of them: ```nupp local wanted = os.getenv("LOG_LEVEL") if wanted == "debug" or wanted == "info" or wanted == "warn" then nupp.log.level(wanted) end ``` A level that is not one of the five is a compile error where it is a literal and an ordinary raise where it is not. `nupp.log.enabled(level)` answers whether a level would emit. Use it to guard preparation spanning more than one call, which no single lowered site can elide: ```nupp if nupp.log.enabled("debug") then local report = summarize(world) nupp.log.debug("world: %s", report) end ``` ## Swapping the back end A host that logs through its own facility installs a sink function and takes over completely. It receives the parts, not a rendered line, and pays for no formatting it would discard: ```nupp nupp.log.sink(function(level: integer, module: string, line: integer, message: string): nil sdl.logMessage(CATEGORY, PRIORITY[level], ("%s:%d %s"):format(module, line, message)) end) ``` `level` is `1` error, `2` warn, `3` info, `4` debug; `nupp.log.levelName` turns one back into its name. `line` is `0` for a line the compiler could not attribute, which is every line from a named logger. Passing anything file-like instead keeps the built-in rendering and only moves where it goes: ```nupp local file = assert(io.open("game.log", "a")) nupp.log.sink(file) ``` `io.open` answers `LuaFile?`, and `sink` takes a target rather than a maybe, so the `assert` is what turns one into the other. A file-like target renders through the formatter, which is replaceable on its own: ```nupp nupp.log.formatter(function(level: integer, module: string, line: integer, message: string, stamp: string): string return ("%s[%s] %s"):format(stamp, nupp.log.levelName(level), message) end) ``` Both setters answer the value they replaced, so a host can restore what it found. ## Timestamps `nupp.log.timestamp()` answers the current time formatted, recomputed at most once per wall-clock second and shared by every logger. It is a pull rather than something pushed to sinks, so a host that stamps its own lines never pays for one. ```nupp nupp.log.timestampFormat("%H:%M:%S ") -- set, answers the previous format nupp.log.timestampFormat("") -- off ``` ::: deepdive Reading the second through the FFI `os.time` is NYI in LuaJIT and stitches the trace it stands on, so the second is read through the FFI instead. `os.time` is still called once at startup to validate the symbol, because some Windows CRTs inline `time` to `_time64` or give it a 32-bit `time_t`, and a symbol that resolves but is the wrong width answers nonsense rather than failing to resolve. ::: ## Named loggers `named` answers a logger carrying a fixed name, with a method per severity: ```nupp local physics = nupp.log.named("physics") physics:warn("step %d took %.2fms", step, elapsed) ``` Use one for a subsystem that does not correspond to a module, and for a call site the intrinsic cannot reach. Repeating a name answers the same logger. Their methods are replaced when the level or target changes, so a filtered call reaches an empty function rather than a test, but the arguments are still evaluated, which is the cost of a name chosen at run time. ## Cost The installer lands only in modules that reach `nupp.log`, like every other compiler-provided facility. A module that never logs carries nothing. ::: seealso - [Diagnostics](../../../reference/diagnostics/index.html#diagnostic-index) for the codes a mistyped format string reports - [Standard library](../../../learn/runtime/data/standard-library/index.html) for how a facility reaches a program without being linked into one that never uses it ::: ## Types ### `Formatter` _type_ ```nupp type log.Formatter = function( level: log.Severity, module: string, line: integer, message: string, stamp: string ): string ``` Renders one line for a file-like destination. Only consulted when the target is file-like; a sink function formats however it likes. ### `Level` _type_ ```nupp type log.Level = "off" | "error" | "warn" | "info" | "debug" ``` The threshold, from silent to most verbose. Each level admits itself and everything above it, so "warn" emits warnings and errors. ### `Logger` _record_ ```nupp record log.Logger readonly name: string debug: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil info: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil warn: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil error: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil enabled: function(self: log.Logger, level: log.Level): boolean end ``` A logger carrying a fixed name, for subsystems and for call sites the intrinsic cannot rewrite. Changing the level or target restamps every logger, so a filtered call reaches an empty function rather than a test. #### Methods ##### `debug` ```nupp debug: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at debug. Accepts `string.format` directives. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `log.Logger` | | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `info` ```nupp info: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at info. Accepts `string.format` directives. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `log.Logger` | | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `warn` ```nupp warn: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at warn. Accepts `string.format` directives. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `log.Logger` | | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `error` ```nupp error: function(self: log.Logger, fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at error. Accepts `string.format` directives. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `log.Logger` | | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `enabled` ```nupp enabled: function(self: log.Logger, level: log.Level): boolean ``` Whether this logger would emit at `level`. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `log.Logger` | | | `level` | `log.Level` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | #### Fields ##### `name` ```nupp name: string ``` The name every line from this logger carries. ### `Severity` _type_ ```nupp type log.Severity = integer ``` A level as a sink sees it: 1 error, 2 warn, 3 info, 4 debug. ### `Sink` _type_ ```nupp type log.Sink = function(level: log.Severity, module: string, line: integer, message: string): nil ``` Receives one emitted line, already formatted. Replacing this replaces the back end, so a host logging through its own facility pays for nothing it discards. No timestamp is passed, because a sink that wants one asks. ### `Target` _type_ ```nupp type log.Target = log.Sink | LuaFile ``` Where lines go: a sink function, or anything file-like to write to. ## Functions ### `log.debug` _function_ ```nupp function log.debug(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at debug. Accepts `string.format` directives. #### Type parameters | Name | Description | | --- | --- | | `F` | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `log.enabled` _function_ ```nupp function log.enabled(level: log.Level): boolean ``` Whether a level is currently admitted. #### Arguments | Name | Type | Description | | --- | --- | --- | | `level` | `log.Level` | the level to test | #### Returns | Type | Description | | --- | --- | | `boolean` | whether a call at that level would emit | ### `log.error` _function_ ```nupp function log.error(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at error. Accepts `string.format` directives. #### Type parameters | Name | Description | | --- | --- | | `F` | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `log.formatter` _function_ ```nupp function log.formatter(next: log.Formatter?): log.Formatter? ``` Reads or replaces the line formatter. #### Arguments | Name | Type | Description | | --- | --- | --- | | `next` | `log.Formatter?` | the formatter, or nil to read the current one | #### Returns | Type | Description | | --- | --- | | `log.Formatter?` | the formatter this call replaced | #### Raises - when next is not a function ### `log.forModule` _function_ ```nupp function log.forModule(module: string): any ``` The view a generated chunk binds once, so a site carries only severity and line. Generated code calls this in a module's prologue. Nothing written by hand needs it: a severity call in statement position is already lowered to a test against the view's `on` array around a direct `emit`. #### Arguments | Name | Type | Description | | --- | --- | --- | | `module` | `string` | the module name its lines carry | #### Returns | Type | Description | | --- | --- | | `any` | the view, whose `on` a site indexes and whose `emit` it calls | ### `log.info` _function_ ```nupp function log.info(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at info. Accepts `string.format` directives. #### Type parameters | Name | Description | | --- | --- | | `F` | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `log.level` _function_ ```nupp function log.level(level: log.Level?): log.Level ``` Reads or moves the threshold. Each level admits itself and everything above it. #### Arguments | Name | Type | Description | | --- | --- | --- | | `level` | `log.Level?` | the new threshold, or nil to read the current one | #### Returns | Type | Description | | --- | --- | | `log.Level` | the threshold this call replaced | ### `log.levelName` _function_ ```nupp function log.levelName(severity: log.Severity): log.Level ``` The name for a sink's numeric level. #### Arguments | Name | Type | Description | | --- | --- | --- | | `severity` | `log.Severity` | the numeric level a sink received | #### Returns | Type | Description | | --- | --- | | `log.Level` | the level's name | ### `log.named` _function_ ```nupp function log.named(name: string): log.Logger ``` A logger with a fixed name. Repeating a name answers the same logger. Reach for one where the intrinsic cannot rewrite the call: a format built at run time, a call in expression position, a subsystem that wants its own name on every line. The methods are restamped whenever the level or the target moves, so a filtered call reaches an empty function rather than a test. ```nupp const log = nupp.log.named("renderer") log:info("loaded %d entities", count) ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | the name its lines carry | #### Returns | Type | Description | | --- | --- | | `log.Logger` | the logger | #### Raises - when name is not a string ### `log.sink` _function_ ```nupp function log.sink(next: log.Target?): log.Target ``` Reads or replaces the destination. #### Arguments | Name | Type | Description | | --- | --- | --- | | `next` | `log.Target?` | a sink function, a file, or nil to read the current destination | #### Returns | Type | Description | | --- | --- | | `log.Target` | the destination this call replaced | #### Raises - when next is neither a sink function nor a file ### `log.timestamp` _function_ ```nupp function log.timestamp(): string ``` The stamp a line written now would carry. #### Returns | Type | Description | | --- | --- | | `string` | the rendered timestamp, or "" when stamping is off | ### `log.timestampFormat` _function_ ```nupp function log.timestampFormat(next: string?): string ``` Reads or replaces the timestamp format. An empty format stops stamping. #### Arguments | Name | Type | Description | | --- | --- | --- | | `next` | `string?` | the `os.date` format, or nil to read the current one | #### Returns | Type | Description | | --- | --- | | `string` | the format this call replaced | #### Raises - when next is not a string ### `log.warn` _function_ ```nupp function log.warn(fmt: F, ...: unpackof __NuppFormatArguments(F, nupp.Debug)): nil ``` Logs at warn. Accepts `string.format` directives. #### Type parameters | Name | Description | | --- | --- | | `F` | | #### Arguments | Name | Type | Description | | --- | --- | --- | | `fmt` | `F` | | | `...` | `unpackof \_\_NuppFormatArguments(F, nupp.Debug)` | | #### Returns | Type | Description | | --- | --- | | `nil` | |