Scoped values and Frame evaluation¶
Scoped values organize related names without creating nested Frames. Complete keys remain flat while each missing prefix appears as a lazy proxy over the same Frame. An existing Frame can also evaluate a quick unnamed LCL expression without adding a Module definition or cache entry.
What you will learn¶
- how qualified keys infer lazy Frame proxies;
- why qualified leaves normally make
FRAME_PROXYunnecessary; - how scoped lookup, safe access, parent lookup, and caching interact;
- how Python and LCL use the same qualified paths;
- how to discover proxy children and materialize dataclass records;
- when to use
frame.evaluateinstead offrame.get.
Define and read one scope¶
The smallest useful scope needs only a qualified leaf. Prefix proxies are inferred automatically.
import asyncio
from lclang.lang import FrameProxy, define_frame, define_module
async def main() -> None:
module = define_module(
"service",
{
"service.host": '"api.example.com"',
"service.port": "8443",
"service.url": 'f"https://{service.host}:{service.port}"',
},
)
async with define_frame(module) as frame:
assert await frame.get("service.url") == "https://api.example.com:8443"
service = await frame.get("service")
assert isinstance(service, FrameProxy)
assert await service.port == 8443
asyncio.run(main())
Resolving service creates only a lightweight proxy bound to frame.
Requesting service.url evaluates that named definition and follows the fully
qualified service.host and service.port dependencies. The terminal result
is cached under service.url; the proxy owns no independent state.
Declare intent and use safe fallback¶
Qualified leaves infer their prefixes. FRAME_PROXY is optional low-level
metadata for a Python Module that must promise a scope even when an optional
descendant may be absent; it is not recommended as ordinary configuration-file
layout.
import asyncio
from lclang.lang import FRAME_PROXY, define_frame, define_module
async def main() -> None:
module = define_module(
"optional-service",
{
"service": FRAME_PROXY,
"service.required": '"ready"',
"summary": "service?.optional ?? service.required",
},
)
async with define_frame(module) as frame:
assert await frame.get("summary") == "ready"
assert await frame.evaluate("service?.optional ?? 'fallback'") == "fallback"
assert await frame.evaluate("lhs()") == "<expr>"
asyncio.run(main())
The declared service prefix is not a real value. Safe access asks the proxy
for service.optional; because the complete hierarchy lacks that leaf, it
returns None and ?? selects the right side. The unnamed evaluations run in
the Frame but do not add snapshots. Their definition context is <expr>.
Override one leaf and recalculate another¶
A proxy always starts lookup from the Frame that produced it. A child can therefore override one scoped leaf while another leaf falls back to its parent.
import asyncio
from lclang.lang import define_frame, define_module
async def main() -> None:
timeout = 30
async def read_timeout() -> int:
return timeout
parent = define_frame(
define_module(
"defaults",
{"service.timeout": "read_timeout()", "service.retries": "2"},
),
preset={"read_timeout": read_timeout},
)
child = parent.derive(
define_module("production", {"service.retries": "4"})
)
try:
service = await child.get("service")
assert await service.timeout == 30
assert await service.retries == 4
timeout = 45
assert await child.get("service.timeout") == 30
assert await parent.recalculate("service.timeout") == 45
assert await child.evaluate("service.timeout + service.retries") == 49
finally:
await child.close()
await parent.close()
asyncio.run(main())
service is bound to the child caller, so terminal traversal checks the child
before the parent. The async host function runs only when service.timeout is
first requested. Its 30 snapshot survives the Python variable change until
explicit parent recalculation publishes 45; the child expression then adds
its local service.retries value.
Reuse named snapshots from an unnamed expression¶
Each frame.evaluate call parses and executes its root expression again.
Named dependencies still use ordinary Frame snapshots.
import asyncio
from lclang.lang import define_frame, define_module
async def main() -> None:
named_calls = 0
root_calls = 0
def produce() -> int:
nonlocal named_calls
named_calls += 1
return 21
def tick() -> int:
nonlocal root_calls
root_calls += 1
return 1
module = define_module("calculation", {"scope.value": "produce()"})
preset = {"produce": produce, "tick": tick}
async with define_frame(module, preset=preset) as frame:
assert await frame.evaluate("scope.value * 2 + tick()") == 43
assert await frame.evaluate("scope.value * 2 + tick()") == 43
assert named_calls == 1
assert root_calls == 2
closure = await frame.evaluate("() -> lhs()")
assert await closure() == "<expr>"
asyncio.run(main())
The unnamed root calls tick on both runs, proving that no root result or
single-flight snapshot is cached. Both calls request the same named
scope.value, whose snapshot makes produce run once. The returned closure
retains <expr> as its lexical lhs() owner.
Configuration and CLI layers¶
A configuration file may define dotted left-hand names directly. This complete example also applies a qualified CLI override.
services.lclcfg:
# scope: service endpoint
service.port: 8000 # Listener port
service.owner: lhs() # Full winning key
# scope: database capacity
database.pool: 4 # Connection count
Run the following program beside the file:
import asyncio
import io
from contextlib import redirect_stdout
from pathlib import Path
from lclang.cli import CliContext, CliEntrance, CliResult, CommandGroup, cli
@cli.command()
async def inspect_command(context: CliContext) -> CliResult:
owner = await context.frame.get("service.owner")
total = await context.frame.evaluate("int(service.port) + database.pool")
return CliResult.success(f"{owner}: {total}")
async def main() -> None:
directory = str(Path.cwd())
path = Path(directory) / "services.lclcfg"
application = CliEntrance(
CommandGroup("root", "Scoped inspection", [inspect_command]),
version="1.0.0",
)
output = io.StringIO()
with redirect_stdout(output):
status = await application.run(
[
"python",
"inspect.py",
"inspect",
"--config",
str(path),
"-o",
"service.port",
"9440",
]
)
assert status == 0
assert output.getvalue() == "service.owner: 9444\n"
print(output.getvalue(), end="")
asyncio.run(main())
The program prints:
service.owner: 9444
The qualified leaves infer both prefixes while the standalone comments explain
their purpose. lhs() records the full winning key service.owner. The CLI
literal wins only for service.port; database.pool falls back to the config
Frame. The handler's unnamed expression reads both scopes without creating a
new definition, and the application closes all invocation Frames.
The equivalent source is:
# scope: service endpoint
service.host: "api.example.com" # Deployment DNS name
service.port: 8443 # TLS listener
service.url: f"https://{service.host}:{service.port}" # Complete URL
service.owner: lhs() # Full definition key
service.owner returns its complete key. CLI applications use the same model:
-o service.port 9443 supplies a literal scoped override,
-o service.port "LCL[base_port + 1]" supplies a lazy definition, and
-o service LCL[FRAME_PROXY] still declares an explicit placeholder for APIs
that need one, but normal CLI configuration should supply qualified leaves.
Real values cannot overlap structurally. service and service.port cannot
both be real, nor can service.port and service.port.value. Exact overrides
and siblings are valid. Validation runs before mutation or evaluation,
including across a Frame's live hierarchy.
Handle validation and evaluation failures¶
Ad hoc evaluation keeps the normal syntax, name, evaluation-limit, and Frame lifecycle errors. An uncached returned resource belongs to the caller.
import asyncio
from lclang.error import LclClosedFrameError, LclEvaluationError, LclNameError, LclSyntaxError
from lclang.lang import define_frame, define_module
from lclang.error import LclValidationError
class Resource:
def __init__(self) -> None:
self.closed = False
async def aclose(self) -> None:
self.closed = True
async def expect(error_type: type[Exception], operation: object) -> None:
try:
await operation
except error_type:
return
raise AssertionError(f"expected {error_type.__name__}")
async def main() -> None:
try:
define_module("conflict", {"service": "1", "service.port": "2"})
except LclValidationError:
pass
else:
raise AssertionError("a real prefix must conflict with its descendant")
resource = Resource()
frame = define_frame(
define_module("errors", {"broken": "value()"}),
preset={"value": 42, "make": lambda: resource},
)
await expect(LclSyntaxError, frame.evaluate("1 +"))
await expect(LclNameError, frame.evaluate("missing"))
await expect(LclEvaluationError, frame.evaluate("broken"))
assert await frame.evaluate("make()") is resource
await frame.close()
assert resource.closed is False
await expect(LclClosedFrameError, frame.evaluate("1"))
await resource.aclose()
assert resource.closed is True
asyncio.run(main())
The structural conflict fails before a Frame exists. Each invalid unnamed
expression reports its ordinary source-aware error; none becomes an ad hoc
failure snapshot. broken is still a named definition, so its own failure uses
normal named caching. The resource returned by make() is never stored in the
Frame, so closing the Frame leaves it open for explicit caller cleanup.
Build an override-friendly list of records¶
A scope can represent a collection without storing one aggregate list. Each item stays independently overrideable, while Python can materialize a typed, detached view when it needs one.
import asyncio
from dataclasses import dataclass
from lclang.lang import define_frame, define_module
@dataclass
class Endpoint:
name: str
host: str
port: int = 443
async def main() -> None:
parent = define_frame(
define_module(
"endpoint-defaults",
{
"endpoints.blue.name": '"blue"',
"endpoints.blue.host": '"blue.example.com"',
"endpoints.blue.port": "8443",
"endpoints.green.name": '"green"',
"endpoints.green.host": '"green.example.com"',
},
)
)
child = parent.derive(
define_module(
"deployment-overrides",
{
"endpoints.green.host": '"new-green.example.com"',
"endpoints.canary.name": '"canary"',
"endpoints.canary.host": '"canary.example.com"',
},
)
)
try:
endpoints = await child.get("endpoints")
names = await endpoints.field_names()
assert names == ["blue", "canary", "green"]
records = [await endpoints[name].as_record(Endpoint) for name in names]
assert records == [
Endpoint("blue", "blue.example.com", 8443),
Endpoint("canary", "canary.example.com"),
Endpoint("green", "new-green.example.com"),
]
assert await child.evaluate('endpoints["blue"].port') == 8443
finally:
await child.close()
await parent.close()
asyncio.run(main())
field_names() inspects only the immediate effective children, deduplicates
names across both Frame layers, and sorts them alphabetically. Indexing a proxy
with a string follows exactly the same lookup path as attribute access, which
is why the LCL expression and endpoints["blue"] reach the same cached leaf.
as_record(Endpoint) resolves only declared initializer fields: the missing
ports use the dataclass default, while unrelated scoped fields would be ignored.
Each Endpoint is a detached result, not a new owner or cache. Lookup,
dependency tracing, lifecycle ownership, caching, and override precedence all
remain in the shared child/parent Frame hierarchy. Keeping each endpoint as
flat qualified bindings lets an override replace one field or add one endpoint
without copying or replacing an aggregate collection.
Choose the right boundary¶
- Use
define_moduleanddefine_framefor a calculation with explicit inputs. - Use
frame.evaluatefor an unnamed expression over an existing context. - Use
frame.getfor a named definition whose outcome should be cached. - Use
FrameProxy.field_names()and indexedas_record()calls for a dynamic, independently overrideable collection of typed results.
Scoped names never introduce relative lookup: inside service.url, host
does not mean service.host. Explicit paths keep dependencies unambiguous.
Previous: Production patterns | Next: Tree workflows | Return to the series introduction