8.1. Logging

Morloc programs can emit per-call log lines around any labeled term in source. Logging is opt-in — a term emits start, pass, and fail messages only after the user wires it up in the program’s YAML config.

The log lines go to stderr, so the program’s stdout (the computed result and any user-printed data) is unaffected. Templates are user-controlled and may include ANSI color codes; colors are automatically stripped when stderr is not a terminal so log files and pipes never contain control bytes.

8.1.1. Enabling logging

Two things turn logging on for a term:

  1. The term must be labeled in source code. A labeled term is written label@term. For example, a@map is the term map with label a. Labels are per-call-site, so (a@foo x, b@foo y) labels two distinct invocations of the same foo.

  2. The label must appear in the program’s .yaml config under labeled-groups with log: true. The config lives next to the .loc source: for main.loc, the config is main.yaml.

A minimal config:

labeled-groups:
  big: { log: true }

A label group can be applied to many terms (big@read, big@parse, big@save); all of them log under the same group.

8.1.2. Template placeholders

The compiler emits up to three lines per labeled call — start (entry), pass (success), and fail (exception). Each line’s text is a user template with {placeholder} substitutions. The default template is:

log-template:
  start: "[{date}] {module}:{line}:{name}:{lang} start"
  pass:  "[{date}] {module}:{line}:{name}:{lang} pass (time={runtime})"
  fail:  "[{date}] {module}:{line}:{name}:{lang} fail (time={runtime})"

Templates resolve in this order, per subfield: per-label override > program-wide log-template (top of the config) > built-in default. A null subfield silences that event. A common case is to silence the verbose start and pass messages and keep only the failure trace:

log-template:
  start: null
  pass:  null
  fail:  "{date} {module}:{line}:{name} FAILED (time={runtime})"

Setting all three subfields to null while keeping log: true is rejected at compile time as contradictory (use log: false instead).

Available placeholders:

Placeholder Value

{name}

The labeled term’s identifier in source (e.g. map for big@map).

{group}

The label group name (e.g. big for big@map).

{lang}

The pool language: py, cpp, r, etc.

{module}

The source file path of the labeled reference.

{line}

Line number of the labeled reference.

{column}

Column number of the labeled reference.

{index}

The manifold ID assigned by the compiler. Useful for cross-referencing with morloc dump output.

{date}

UTC ISO 8601 timestamp at the moment of emission, second resolution (e.g. 2026-06-08T16:59:59Z).

{runtime}

Elapsed time in seconds since the call’s start, with microsecond precision. Pass 0.0 at the start event.

{id}

Call id pairing a start with its pass/fail. Format {pid}:{counter}; unique within a pool process.

Unknown placeholder names are a compile-time error citing the file and line of the offending YAML entry.

8.1.3. Color codes

A {c:NAME} placeholder expands to the corresponding ANSI SGR escape sequence at compile time. Apply a color, render the text, then reset with {c:reset}:

start: "{c:blue}{name}{c:reset} start"

Attributes and clears:

Placeholder Effect

{c:reset}

Reset every attribute to terminal default.

{c:bold}

Bold / increased intensity.

{c:dim}

Faint.

{c:italic}

Italic.

{c:underline}

Underline.

{c:blink}

Slow blink.

{c:rapid-blink}

Rapid blink. Spotty terminal support; prefer {c:blink}.

{c:reverse}

Swap foreground and background.

{c:hidden}

Conceal text (still occupies space).

{c:strike}

Strikethrough.

{c:no-bold}

Cancel bold (ANSI conflates with dim; see {c:no-dim}).

{c:no-dim}

Cancel dim (same SGR code as {c:no-bold}).

{c:no-italic}

Cancel italic.

{c:no-underline}

Cancel underline.

{c:no-blink}

Cancel blink.

{c:no-reverse}

Cancel reverse.

{c:no-hidden}

Cancel conceal.

{c:no-strike}

Cancel strikethrough.

Foreground colors:

Standard Bright

{c:black}

{c:bright-black} (alias: {c:gray}, {c:grey})

{c:red}

{c:bright-red}

{c:green}

{c:bright-green}

{c:yellow}

{c:bright-yellow}

{c:blue}

{c:bright-blue}

{c:magenta}

{c:bright-magenta}

{c:cyan}

{c:bright-cyan}

{c:white}

{c:bright-white}

{c:default}

 — 

Background colors:

Standard Bright

{c:bg-black}

{c:bg-bright-black}

{c:bg-red}

{c:bg-bright-red}

{c:bg-green}

{c:bg-bright-green}

{c:bg-yellow}

{c:bg-bright-yellow}

{c:bg-blue}

{c:bg-bright-blue}

{c:bg-magenta}

{c:bg-bright-magenta}

{c:bg-cyan}

{c:bg-bright-cyan}

{c:bg-white}

{c:bg-bright-white}

{c:bg-default}

 — 

8.1.4. Terminal detection and NO_COLOR

Color codes from {c:…​} placeholders (or raw ANSI escapes a user writes directly into a template) are emitted unchanged when stderr is a terminal and the environment variable NO_COLOR is unset. In every other case — stderr redirected to a pipe or file, or NO_COLOR set to any value — the runtime strips all CSI sequences before writing, so the output is plain text.

This means the same program produces colored output when run interactively:

./main '[[1,2],[3,4,5]]'      # colors emitted to terminal

and plain text when piped or redirected:

./main '[[1,2],[3,4,5]]' 2> run.log    # run.log is plain text
./main '[[1,2],[3,4,5]]' 2>&1 | grep pass    # grep sees plain text

To suppress color even in an interactive terminal (e.g. for screen captures, color-blind users, or terminals with non-standard palettes), set NO_COLOR:

NO_COLOR=1 ./main '[[1,2],[3,4,5]]'

NO_COLOR follows the convention at no-color.org: any non-empty value disables color; the variable being unset means color is allowed.

8.1.5. Worked example

The source labels two terms — a@map and b@sum — in a small two-stage pipeline that sums each inner list:

module main (foo)

import root-py

sum :: [Real] -> Real
sum = fold (+) 0.0

foo :: [[Real]] -> [Real]
foo = a@map b@sum

The config enables both labels and uses a maximalist template that exercises every color category and every placeholder:

log-template:
  start: "{date} {c:gray}{module}:{line}:{column}:{lang}{c:reset} {name}:{id}: {c:blue}start{c:reset}"
  pass:  "{date} {c:gray}{module}:{line}:{column}:{lang}{c:reset} {name}:{id}: {c:green}pass{c:reset} {c:grey}(time: {runtime}){c:reset}"
  fail:  "{date} {c:gray}{module}:{line}:{column}:{lang}{c:reset} {name}:{id}: {c:red}fail{c:reset} {c:grey}(time: {runtime}){c:reset}"

labeled-groups:
  a: { log: true }
  b: { log: true }

Build and run on a two-row input:

morloc make -o main main.loc
./main '[[1,2],[3,4,5]]'

Output to stderr, rendered with the colors a terminal would show (one start/pass pair per call; a@map brackets two inner b@sum calls; the nested call ids 0, 1, 2 pair start lines with their corresponding pass lines):

2026-06-08T16:59:59Z main.loc:9:7:py map:33804:0: start
2026-06-08T16:59:59Z main.loc:9:13:py sum:33804:1: start
2026-06-08T16:59:59Z main.loc:9:13:py sum:33804:1: pass (time: 0.000023)
2026-06-08T16:59:59Z main.loc:9:13:py sum:33804:2: start
2026-06-08T16:59:59Z main.loc:9:13:py sum:33804:2: pass (time: 0.000009)
2026-06-08T16:59:59Z main.loc:9:7:py map:33804:0: pass (time: 0.000520)

Output to stdout (the actual program result):

[3,12]

Run the same command with NO_COLOR=1 for plain output even when stderr is a terminal:

NO_COLOR=1 ./main '[[1,2],[3,4,5]]'