Runtime API guide

Import layers

lclang.lang exposes the preferred define_module/define_frame workflow, the canonical LCL_ROOT, LCL_BUILTINS, LCL_RUNTIME, and LCL_IMPORTS Frames, and the lower-level Module, Frame, FrameFactory, Preset, EvaluationLimits, DependencySnapshot, LclRecord, NO_FALLBACK, and STANDARD_PRESET APIs.

Use lclang.lang.runtime for advanced static graph construction, topological order, dynamic tracing, reconciliation values, and standard runtime types. Use lclang.lang.stdlib for manifests, namespace assembly, individual reviewed helpers, STANDARD_MANIFESTS, and STANDARD_PRESET.

Public evaluation boundary

Define named expressions with define_module, create their runtime context with define_frame, and request results with await frame.get(name). Even a single calculation uses this path, so it receives the same canonical builtins, host bindings, caching, diagnostics, and cleanup as a larger configuration. Use asyncio.run once at a synchronous application's outer boundary; async applications await their existing coroutine instead.

The standalone evaluate and evaluate_sync functions have been removed from the public API. Migrate source expressions to named Module definitions and resolver mappings to the Frame's preset bindings. For an unnamed expression within an existing context, use await frame.evaluate(source).

Record values

{a=expression, b=expression} evaluates to lclang.lang.LclRecord. LCL and Python both read fields with ordinary attributes, such as record.a. The public LclRecord(fields) constructor copies one non-empty mapping whose keys obey the same identifier, reserved-word, and __ restrictions as record syntax. Records expose no mapping or subscription protocol. Assignment and deletion fail, while nested values retain their original identity and mutability. Equality and hashing compare field names and values without declaration order; hashing fails normally when a field value is unhashable. repr retains declaration order, for example LclRecord(a=1, b=2).

Preferred definitions and canonical hierarchy

define_module(name, exprs) parses a string-keyed dictionary into an immutable Module. Values are expression strings or the singleton declaration markers FRAME_PROXY, NEED_OVERRIDE, and RUNTIME_OVERRIDE. define_frame(module=None, base=LCL_RUNTIME, preset=None) creates a fresh user Frame. Lookup proceeds from the nearest layer through:

user module -> LCL_IMPORTS -> LCL_RUNTIME -> LCL_BUILTINS -> LCL_ROOT.

LCL_ROOT owns definition-scoped lhs(). LCL_BUILTINS owns the reviewed standard and calendar namespaces plus curated Python types/functions, including strict parse_ymd/to_ymd date conversion and the variadic eager fixed-point helper recursive; LCL_RUNTIME is the empty package default and may be replaced with one CLI-aware base per run; LCL_IMPORTS is the detached preset layer. User definitions therefore win over presets, which win over runtime values. Canonical ancestors are borrowed and closing a user Frame never closes them.

LCL_BUILTINS also exposes SnowflakeGenerator(worker_id, *, epoch_ms=1704067200000). Retain the constructed instance and call next_id() for clock-backed integer identifiers. The Snowflake utility contract defines worker assignment, uniqueness conditions, failure handling, and Frame ownership.

Definition and host-value names may be qualified paths such as service.database.port. The runtime stores the complete spelling as one flat binding. Missing prefixes are lazy FrameProxy views over the requesting Frame; they create no nested Frames or separate caches. FRAME_PROXY may be used as an optional complete binding value to document a prefix explicitly. Real ancestor/descendant binding pairs are rejected eagerly.

Python may use await frame.get("service.database.port"), or obtain the root proxy and use await service.database.port. String indexing is identical, so await service["database"].port in Python and service["database"].port in LCL retain the same lookup, tracing, caching, and errors. A non-string proxy index raises LclValidationError; a missing attribute or index raises the same LclAttributeError. References remain fully qualified: service.total and total are separate.

await proxy.get(name, default=None) performs direct-child lookup and returns the default only when that child is absent. await proxy.field_names() returns the sorted, unique immediate effective child identifiers across the Frame hierarchy, including inferred prefixes but excluding the proxy marker. await proxy.as_record(DataclassType) resolves present initializer fields in declaration order and constructs a detached dataclass instance. Absent fields are omitted so dataclass defaults and default factories apply; extra children, non-initializer fields, ClassVar, and InitVar do not participate. The type must be a dataclass, missing required constructor values fail normally, and child evaluation errors propagate.

await frame.evaluate(expr) parses and evaluates one unnamed LCL expression against an open Frame. The expression itself is not cached or entered into a dependency snapshot, although named definitions reached through it retain their normal snapshots and flights. lhs() returns "<expr>" during this evaluation and in closures it creates.

The fixed builtin inventory also includes the distinct env environment utility. env.NAME reads the live process environment and returns None when absent; env.get("non-identifier", default) supports arbitrary environment names. Scoped definitions and values named env.NAME win, including explicit None, without mutating os.environ. env.NAME ?? default is the ordinary LCL fallback form, and env.field_names() includes valid current environment names plus effective scoped overrides.

The remaining fixed builtin inventory is: value types bool, bytes, dict, float, frozenset, int, list, set, str, and tuple; functions abs, all, any, bin, chr, divmod, enumerate, filter, format, hex, isinstance, len, map, max, min, oct, ord, parse_ymd, pow, range, recursive, repr, reversed, round, slice, sorted, sum, to_ymd, and zip. File/process input, dynamic import/code, reflection, and mutation helpers are deliberately absent.

recursive(builder) returns a callable eager fixed point. Its representation is Recursive Function: <original source>: an LCL function builder uses canonical LCL source, while a Python builder uses its function name. Frame inspection uses this representation directly and never includes a Python object address.

Anonymous LCL functions use () -> expression, name -> expression, or (parameters) -> expression. The bare name is only a single required positional parameter shorthand. Canonical source always parenthesizes the signature.

Explicit Module, Preset, and FrameFactory

Module copies a name-to-AST mapping into a read-only snapshot. Preset does the same for host bindings; overlay is shallow and right-biased. A single trailing ! on a binding key marks its normalized exact name as masked, so {"token!": value} creates the runtime name token. Modules, Presets, Frames, Frame mixins, and CLI binding sources share this spelling. Their immutable masked_names metadata retains the policy, and frame.is_masked(name) checks it without evaluating the name. Once marked in an effective Frame hierarchy, a same-name override stays masked; unrelated or derived names do not inherit the flag. Direct Frame(..., masked_names=...) and frame.derive(..., masked_names=...) calls may also overlay an exact inherited name, which lets an application boundary redact a borrowed parent value without copying it. Module and Preset trailing-! declarations still name bindings in their own source. A FrameFactory retains a Module, optional Preset, and optional default EvaluationLimits. Each create call has fresh cache, task, dependency, and lifecycle state. Call-level values and limits override factory policy. Parent Frames are borrowed, not owned. create() and direct Frame(module) both default their ID to frame-<module name>; an ordinary non-empty string or FrameId overrides that diagnostic default.

Prefer async with parent.derive(module, values={}) as child: for an ordinary owned child Frame. It copies local host values, uses the module name as the child ID, owns fresh cache/task/dependency/lifecycle state, and borrows parent. Leaving the child block closes only that child; the parent remains open. Use direct Frame construction when child-specific limits or values are required.

Frame caching and concurrency

Module.mixin(other, *, name=None) returns a new immutable Module. Definitions on the right replace same-name definitions on the left, while names keep their first-appearance order. The default name is the left Module's name. Masks remain sticky, explicit namespace_names are combined, and the complete structure is validated before return. Existing Modules and Frames retain their snapshots. Module.namespace_names is a keyword-only frozenset, empty by default; it reserves namespaces even when they have no fields.

NEED_OVERRIDE and RUNTIME_OVERRIDE are allowed only as complete right-hand expressions. Both fail lazily with <name> needs a value when requested without a provider. NEED_OVERRIDE blocks lower-priority preset or code fallback; replace its definition with a later configuration or CLI definition, or use Module composition. RUNTIME_OVERRIDE allows actual host values, including None, from a Frame, preset, or later Frame.mixin(). Its declaration discards any earlier configuration definition. Lookup and inspection report the selected host provider even if a placeholder previously failed. Ordinary Module definitions continue to take priority over host values in their own Frame.

await frame.get(name, fallback=NO_FALLBACK) lazily evaluates a selected definition once, caching either its result or ordinary failure. If name is absent from the complete Frame hierarchy, an explicitly supplied fallback is returned unchanged and is not cached. None is a valid fallback. The exported NO_FALLBACK sentinel is the default and preserves the normal LclNameError. Fallbacks do not replace failures raised while evaluating an existing definition.

Concurrent callers in one event loop share one owner task. Waiter cancellation is isolated. Circular dependency paths raise a structured error before an owner can deadlock. Parent definitions always run in their defining Frame.

Structured errors raised while evaluating Frame definitions expose variable_stack, an immutable direct-to-failing owner tuple. Multiline diagnostics start with Error in evaluating <name> [<code>: <reason>] and show the failed dependency path as a tree. Already-read values are leaves; the source excerpt appears at the innermost failure. Lazy child definitions and LCL closure calls add their lexical owner; propagation and cached failures retain the first, deepest stack. An unnamed frame.evaluate expression uses "<expr>" as its owner. Fresh errors include that owner, and failures reached through named definitions add their owners; previously cached failures retain their original stack.

LclError retains message, span, code, and variable_stack, and adds keyword-only config_stack and evaluation_context tuples, empty by default. The frozen, validated ConfigLoadFrame, EvaluationContextFrame, and DiagnosticValue records expose file introductions, active expressions or function calls, and already-read values. Each value has its actual name, source occurrence, concrete type label, protected representation and actual source category. Repeated reads at one occurrence keep their first position and last successful value. Unread short-circuit branches and unresolved values contribute no read evidence.

Snapshots are fixed at failure time. Normal execution does not call repr for diagnostics; failure capture redacts first, applies the existing 200-character single-value budget, and tolerates failed representations. The records retain no Frame, resolver, AST, or host object. Rendering performs no lookup, evaluation, or file access. Cached failures retain their first scene and exception identity; recalculation creates a new scene. Concurrent evaluations have independent read journals. A masked function also protects its invocation arguments and body.

SourceSpan.snapshot is optional keyword-only SourceSnapshot(text, start) metadata. It does not affect span comparison, and parsing preserves a supplied SourceOrigin object. Source-less manual ASTs use canonical expressions without invented file coordinates. CLI and logs use the same multiline diagnostic; logs retain the original Python traceback on the exception. The default formatter shows application and external-library frames; traceback_style="full" also shows lclang frames. Inspection uses a protected single-line failure summary.

Actual Python calls appear beneath the LCL expression that invoked them, including nested LCL requests made by the callback. Definition locations come from Python code metadata; diagnostics do not read source files to locate functions. Applications can opt into the same uncaught-error reports with install_exception_hooks from lclang.logger; see text display.

All synchronous and asynchronous iterable items pass through the recursive auto-await boundary before comprehensions, starred expansion, or reviewed iter.collect/iter.first helpers consume them. In particular, [*map((x) -> x.lower(), ["A", "B"])] evaluates to ["a", "b"] without retaining coroutine objects.

frame.has(name) checks whether that same recursive lookup selects either a definition or host value. frame.get_definition(name) returns the selected custom AST without evaluation, or None when the name is missing or a nearer host value masks an ancestor definition. Both methods preserve all cache, dependency, task, and lifecycle state.

frame.inspect_variable(name) returns a VariableInspectionTree whose direct children are unique by first-seen variable name. Each node exposes its status (Cached, NotEvaluated, ExternalProvided, or NativeProvided), selected AST, exact lookup path, owning Frame, current value or exception, and static dependency children. Missing names remain diagnostic leaves and cycles stop only on the repeating branch; the same name can still appear independently beneath separate parent branches. Inspection never evaluates or awaits anything, joins work, changes caches, or publishes traces. repr(tree) is one protected name: payload (source) line. Unevaluated definitions use canonical expression text, actual source location, the AST type and not evaluated. Cached, external, native and proxy values retain their actual source labels and typed payloads. Cached failures show a short exception reference with the code and reason on the same line.

Supported AST values and LCL closures use canonical LCL source without exposing resolver or evaluator state. Reviewed builtins use Builtin Function, Builtin Namespace or Builtin Utility labels; host values retain typed reprs. tree.to_lines() uses shared tree connectors and alphabetical dependency order. Explicit depth and prefix still select an indented custom list marker. Cached and masked nodes stop expansion, while trusted callers retain the raw structured fields and static dependency children. Masking remains diagnostic redaction rather than access control.

See text diagnostics for runtime trees, failure paths and logging interfaces. Ordinary value repr and canonical source printing keep their existing meanings.

frame.mixin(values) atomically copies a right-biased dictionary into the open Frame's host bindings. The existing frame.values view remains read-only while reflecting the update. Direct lookups and uncached definitions see mixed values; already cached definition successes/failures remain snapshots. Call recalculate explicitly when a definition should observe a new mixed input. Module definitions continue to shadow same-name host values.

Mixin validation includes every open descendant that borrows the updated Frame. A conflicting update is rejected before any value changes.

await frame.recalculate(name) atomically replaces only the named definition's snapshot; it never invalidates a dependant. Old values remain readable during refresh. Ordinary success/failure replaces value/failure and dependency trace; owner cancellation preserves the prior snapshots.

Dependency snapshots

frame.dependency_snapshot(name) is synchronous point-in-time inspection. It returns static edges, observed dynamic edges, and confirmed/inactive/unexpected reconciliation. Before evaluation all static edges are inactive. Cached reads do not add observations. Deferred function/generator lookups extend their defining source's published trace. Parent lookups route to the owner.

lclang.lang.runtime.build_dependency_graph(frame) performs a separate static, non-evaluating hierarchy analysis. It returns FrameDependencyGraph with qualified FrameDependencyBinding definitions/value terminals and FrameDependencyEdge occurrences. Each edge contains lookup_path, its requested target_name, and the selected binding or None. Module input keeps returning the original unqualified DependencyGraph.

See the dependency analysis tutorial for the progressive static-to-runtime workflow and expected results.

Limits and close

EvaluationLimits caps semantic AST depth, node visits, and one materialized collection. One root evaluation chain shares its task-local budget; cached reads consume none and recalculate starts fresh.

Frames implement the asynchronous context-manager protocol. Prefer async with lclang.lang.define_frame(...) as frame: so leaving the block always awaits owned cleanup. The context manager returns the same Frame and never suppresses an exception raised by the block.

await frame.close() is the lower-level explicit equivalent. It rejects new work, cancels and settles owned tasks, and cleans cached resources once in reverse acquisition order. It recognizes sync close and async aclose. Parent resources remain parent-owned. Cleanup errors are stable, and cancelling a waiter does not cancel shared close work.

Standard preset and errors

STANDARD_PRESET independently exposes pure-data iter, text, data, and json namespaces. Canonical Frames merge them into LCL_BUILTINS. The preset has no file, environment, network, subprocess, reflection, or dynamic-import helpers. Individual helpers validate their input protocols and their failures become source-aware structured evaluation failures at the interpreter boundary.

LCL is for trusted application configuration. It is not a security sandbox for hostile expressions. Values and callables supplied by the host can execute ordinary Python behaviour, including blocking or side effects, so applications must review their own Presets.

See errors and diagnostic codes for exception fields, specific codes, cause chains, and executable troubleshooting examples. Ordinary library validation uses LclValidationError; native callback and operation failures retain their original exception in __cause__.

The exception guide describes each failure family, grouped execution and cleanup failures, actual diagnostic output, and application handling with except* and native control signals.