The LCL language¶
LCL is expression-only and familiar to Python readers, but it is not Python source. lclang tokenizes, parses, prints, and interprets its own immutable AST. Every complete expression produces a value or raises a value.
What you will learn¶
- the core literal, collection, operator, and access forms;
- how conditionals, null coalescing, and safe attributes express policy;
- how comprehensions and arrow functions build richer transformations;
- which Python statement forms are deliberately absent.
Core expression families¶
| Need | LCL form | Example result |
|---|---|---|
| Arithmetic | price * quantity |
24 |
| Conditional | a if enabled else b |
one selected branch |
| Null fallback | value ?? default |
first non-None value |
| Safe attribute | profile?.name |
None when profile is None |
| Collection | [a, b], {key: value} |
list or dictionary |
| Record | {name=value} |
immutable attribute value |
| Slice | items[1:4] |
selected range |
| Function | (x, scale=2) -> x * scale |
lexical callable |
| Comprehension | [f(x) for x in xs if keep(x)] |
transformed list |
Boolean operators short-circuit. Conditional expressions evaluate only the selected branch. This matters when the skipped branch is expensive or would fail for the current input.
Combine safe access, fallback, and formatting¶
The host may provide an object or None. Safe attribute access handles the
missing object, ?? supplies a fallback, and an f-string formats the result.
import asyncio
from types import SimpleNamespace
from lclang.lang import define_frame, define_module
GREETING = define_module(
"greeting",
{"message": 'f"Hello, {profile?.display_name ?? fallback}!"'},
)
async def main() -> None:
async with define_frame(
GREETING,
preset={"profile": SimpleNamespace(display_name="Ada"), "fallback": "friend"},
) as known:
assert await known.get("message") == "Hello, Ada!"
async with define_frame(
GREETING,
preset={"profile": None, "fallback": "friend"},
) as anonymous:
assert await anonymous.get("message") == "Hello, friend!"
asyncio.run(main())
For known, safe access obtains Ada, so ?? never evaluates the fallback.
For anonymous, accessing through None produces None, which makes ??
select friend. The same f-string then wraps the selected name in the two
asserted greetings.
?. suppresses only access on None. It does not hide a missing attribute on
a real object. ?? selects its right side only when the left result is None;
false, zero, and empty text remain valid values.
Build an immutable record with transformed values¶
This example filters even values, calls a local arrow function, sorts the result through canonical builtins, and returns both details and a total as named fields.
import asyncio
from lclang.lang import LclRecord, define_frame, define_module
SUMMARY = define_module(
"summary",
{
"result": (
"{"
"squares=sorted([(value -> value * value)(item) "
"for item in values if item % 2 == 0]), "
"total=sum([(value -> value * value)(item) "
"for item in values if item % 2 == 0])"
"}"
),
"record_total": "{value=result.total}.value",
},
)
async def main() -> None:
async with define_frame(SUMMARY, preset={"values": [5, 2, 4, 3]}) as frame:
result = await frame.get("result")
assert isinstance(result, LclRecord)
assert result.squares == [4, 16]
assert result.total == 20
assert await frame.get("record_total") == 20
asyncio.run(main())
The comprehension rejects 5 and 3 because they are odd. The arrow function
squares 2 and 4, sorted orders those values as [4, 16], and the second
comprehension feeds the same values to sum, producing 20. The LCL record
creates both fields eagerly; Python reads them through result.squares and
result.total, while the final assertion demonstrates the same dotted access
inside LCL. sorted and sum come from the Frame's canonical builtin layer;
the preset contains only the application's input list.
Arrow functions use () -> expression, name -> expression, or
(parameters) -> expression. They support defaults, keyword calls, lexical
closures, and deferred bodies. A generator expression stays lazy; list, set,
and dictionary comprehensions materialize results.
Build and inspect syntax without running it¶
Canonical printing is useful for formatters, diagnostics, and code review.
import asyncio
from lclang.lang import define_frame, define_module, parse_expression, to_source
node = parse_expression("(base+tax)*quantity")
source = to_source(node)
assert source == "(base + tax) * quantity"
CALCULATION = define_module("calculation", {"total": source})
async def main() -> None:
async with define_frame(
CALCULATION,
preset={"base": 8, "tax": 2, "quantity": 3},
) as frame:
assert await frame.get("total") == 30
asyncio.run(main())
The printer retains the parentheses because addition must occur before
multiplication. Parsing and printing need no Frame. To calculate the rendered
source, the Module gives it a name and the Frame supplies the three inputs.
base + tax becomes 10, then quantity scales it to 30; both the
normalized syntax and the value are asserted.
Deliberate boundaries¶
LCL excludes assignment statements, imports, classes, loops, lambda, direct
await, and Python AST injection. Use expressions, comprehensions, arrow
functions, expression-form try and with, and narrow host capabilities.
Those constraints keep configuration recognizable as configuration while the
available expression forms still support complex trusted policy.
For the exact grammar, precedence table, control forms, and rainy cases, use the language reference.
Previous: Modules and Frames | Next: Configuration files | Return to the series introduction