Text diagnostics and dependency trees¶
Verbose logs, exceptions, variable inspection and CLI trees share tree connectors,
source positions and protected value summaries. Definitions use
name: expression (location). Cached, external and shared values are leaves:
name: (type) value (source). Arguments and native values retain their own source
labels. Unknown provenance is displayed as unknown; it is never inferred.
Runtime trees contain dependencies that actually ran, sorted by
(name.casefold(), name). Static inspection includes static dependencies and marks
unevaluated definitions. Cached inspection nodes stop expansion; their structured
fields still retain the static children. Workflow trees retain execution order.
Verbose events are DEBUG records displayed as TRACE by the framework formatter. The default verbose prefix retains the timestamp, process and thread. Root requests produce one complete successful runtime tree. First failures show a short reason, failure identifier and execution context; exception output expands the failing path and its already-read value leaves. Source excerpts appear only at the innermost failure. Logging never evaluates extra branches or reloads sources.
Failure propagation produces one summary per failure at the end of a root request, with the names of bindings whose failure caches were actually committed. Individual propagation relations and cache owners remain in detached record context. A root that recovers is not described as failed. Cache hits and shared returns stay explicit.
Each LCL node shows its Context notes first, in their original addition order, before Cause, source, operation, dependency and traceback branches. Nodes without notes have no Context placeholder. This ordering also applies to nested LCL causes and exception-group members. Exception trees then show the explicit Cause before expanding the diagnostic stack. An unsuppressed handling context takes that position when there is no explicit cause. Python traceback trees follow the same order. Stack frames, notes and exception-group members retain their original evidence and ordering.
Every human diagnostic error code has its short reason on the same line. Failure
identifiers correlate verbose summaries, cache returns and exception output.
repr(error) is a protected single-line summary; str(error) is the full tree.
Ordinary LCL values and canonical to_source keep their existing meaning.
Successful evaluations emit one completion event, followed by one tree for the root request. Dynamic using targets emit a single resolution event with the target path, declaration location and evaluation ID. Their expressions and dependencies remain in detached record metadata. Failed targets retain the expression, read values and loading route in the exception tree.
Exception groups share identical evaluation prefixes and branch in member order. Failure IDs correlate reports; they do not suppress new evidence added by a wrapper. Each distinct failure retains its cause and innermost source excerpt. Python callables show their actual implementation's code location when available. Diagnostics do not open Python source files to locate a callable.
Logger and CLI controls¶
Unhandled application exceptions¶
install_exception_hooks(*, traceback_style="compact", include_threads=True,
loop=None) installs explicit stderr reporting. Import it from lclang.logger.
The returned ExceptionHookInstallation.restore() restores only handlers still
owned by that installation. Native exceptions and control signals delegate to
the previous handler. Passing a loop selects that loop alone; install and restore
its handler on the loop's owning thread.
Install hooks at application startup and keep them active until Python reports
an uncaught failure. Restoring in a finally block before the failure reaches
sys.excepthook would disable that report. Applications that catch failures can
use LclExceptionFormatter instead. CLI already reports caught failures at its
execution boundary and does not install these hooks.
import io
import sys
from contextlib import redirect_stderr
from lclang.error import LclError
from lclang.logger import install_exception_hooks
previous = sys.excepthook
installation = install_exception_hooks(include_threads=False)
output = io.StringIO()
try:
try:
raise LclError("billing failed", code="APP/BILLING")
except LclError as error:
original_trace = error.__traceback__
with redirect_stderr(output):
sys.excepthook(type(error), error, original_trace)
assert error.__traceback__ is original_trace
finally:
installation.restore()
assert sys.excepthook is previous
assert "[APP/BILLING: billing failed]" in output.getvalue()
assert "Python traceback:" in output.getvalue()
The hook and logger formatter show the LCL tree first, then application and external-library Python frames. Compact mode removes language implementation frames by module identity. Full mode restores them. Both leave the exception's original traceback, cause, context and group members available to the caller.
LoggerHandlerConfig.traceback_style accepts "compact" (default) or "full".
The same field is available in mappings, logger.traceback_style in LCL and CLI
-o logger.traceback_style full. Compact tracebacks omit lclang Python frames by
module identity and retain application and external-library frames. Full mode
retains all Python frames. Both modes retain native chains and group members,
without repeating complete LCL diagnostics or changing original exceptions.
When a wrapper and its Direct cause select exactly the same traceback nodes,
only the cause's stack is displayed. Distinct wrapper frames remain visible in
both policies; matching rendered text alone never establishes duplication.
External logging callers can install LclExceptionFormatter on their own handler.
It accepts standard formatter arguments and keyword traceback_style. Borrowing
a logger through use_verbose never installs a formatter or changes handlers.
Ordinary failures while checking a verbose destination, constructing a diagnostic,
or emitting its record are discarded. Verbose reporting preserves successful
results, cached values and failures, and the original execution exception. It
does not retry reporting through a failing logger. Cancellation, exit,
interruption and iterator-control signals retain their native behavior. Public
logger and switch validation still applies; explicit display requests and normal
lifecycle/error logging retain their own contracts.
LogRecord.levelno and the original levelname remain DEBUG. The stable verbose,
stage, evaluation, failure and context extras support external filtering; tree
records include detached tree data for external formatting.
import asyncio
import io
import logging
from lclang.error import LclError
from lclang.lang import define_frame, define_module
from lclang.logger import LclExceptionFormatter, use_verbose
async def main() -> None:
output = io.StringIO()
logger = logging.Logger("pricing", logging.DEBUG)
handler = logging.StreamHandler(output)
handler.setFormatter(LclExceptionFormatter("%(levelname)s %(message)s"))
logger.addHandler(handler)
module = define_module("pricing", {"RESULT": "total + 1", "total": "subtotal / divisor"})
async with define_frame(module, preset={"subtotal": 39, "divisor": 0}) as frame:
with use_verbose(logger):
try:
await frame.get("RESULT")
except LclError as error:
failure_id = error.verbose_failure_id
assert failure_id is not None
logger.exception("Pricing request failed")
try:
await frame.get("total")
except LclError as cached:
assert cached is error and cached.verbose_failure_id == failure_id
else:
raise AssertionError("The zero divisor must fail")
text = output.getvalue()
assert "TRACE Failed while evaluating total [LCL131421: division by zero]" in text
assert text.count("Error in evaluating RESULT") == 1
assert "Returned the cached failure of total; no new evaluation" in text
assert "Could not evaluate RESULT" in text
assert text.count("Cached failures: total, RESULT") == 1
assert "Could not complete" not in text
assert "Reason:" not in text
assert "total: subtotal / divisor (" in text
asyncio.run(main())
The first verbose failure explains the arithmetic stage and requesting context.
The root summary lists total and RESULT after both failure caches have committed.
The explicit exception record adds the full failed dependency path and retained
value snapshots. Both use the same failure identifier; the later cache read adds
a short reference. The logger and its handler belong to this application.
eval_lcl retains value stdout. Add -o TREE for the actual runtime tree.
parse_lcl shows static inspection; -o EVAL shows the actual evaluated tree,
including ordinary failures, with its existing successful inspection exit status.
Cached and shared roots remain leaves in either runtime tree mode.
CLI execution and cleanup failures are reported together after cleanup. Stderr receives one complete diagnostic. File output adds Python stack evidence and short LCL references. A later output failure is reported separately and references the earlier failure. Native cancellation and protocol controls still propagate.