An expression produces a value. The grammar and associativity are in grammar.md §6; this document is the semantics of each form: what it means and what it compiles to. Every “compiles to” below is the exact Python emitted by transpiler/transformers/expressions.py, verified by transpiling the shown source (probe).
1. Precedence (lowest to highest)
This table is the parser’s _PRECEDENCE table verbatim (to_ast.py:465-488). Higher number binds tighter. It matches grammar.md §6.1.
* ?? is right-associative and ?: is left-associative — the parser’s is_left_assoc returns False for ?? and True for ?: (to_ast.py:2654-2655). a ?? b ?? c → _aura_null_coalesce(a, _aura_null_coalesce(b, c)); a ?: b ?: c → _aura_elvis(_aura_elvis(a, b), c) (probe).
Comparison is looser than the bitwise group and the shifts, exactly as in Python: 1 & 2 == 2 → ((1 & 2) == 2) and 1 < 2 | 3 → (1 < (2 | 3)) (to_ast.py:472-476, probe).
Comparison does not chain: a < b < c → ((a < b) < c). Write a < b and b < c (to_ast.py:2519-2628, probe).
Pipe is the loosest expression operator, grouped with assignment at level 1 and parsed specially so it chains left to right (to_ast.py:2558-2563). It is looser than ??: a |> f ?? b → _aura_null_coalesce(f, b)(a) (probe).
not binds looser than comparison and bitwise but tighter than and: not a in b → (not (a in b)), not a and b → ((not a) and b) (to_ast.py:2487-2494, probe).
2. Literals
Primary literals are IntLiteral, FloatLiteral, StrLiteral, FStringLiteral, BoolLiteral, NoneLiteral, and the collection literals of §10. Their concrete syntax is in lexical-structure.md §4 and grammar.md §1.4.
Source
Compiles to
Evidence
42
42
transform_IntLiteral
3.5
3.5
transform_FloatLiteral
"hi"
'hi'
transform_StrLiteral
true / false
True / False
transform_BoolLiteral
none
None
transform_NoneLiteral
f"x{1+2}"
f'x{1 + 2}'
transform_FStringLiteral
none is the null literal.null → pointed parse error “use none” (to_ast.py:2686-2688); True/False/None are reserved spellings (to_ast.py:536-540).
F-string fields are parsed recursively (to_ast.py:2855-2867); a !r/!s/!a conversion and a :format spec are preserved (to_ast.py:2884-2918). An invalid field expression is a parse error.
3. Identifiers, member access, indexing, slicing
x — local, parameter, module member, or field. A bare reference to a non-local module member resolves to Module.name (transform_Identifier, probe: return f"{owner}.{py_safe_name(node.name)}"). A name that is a Python keyword (raise, class, …) is emitted with a trailing underscore (py_safe_name).
a.b — member access → a.b (transform_MemberExpr). Protocol method names (str, eq, …) map to their dunder when the class declares them (transform_MemberExpr:552-554).
super — zero-arg super renders as super() (_render_object) so super.new(...) / super.m() work; super(args) renders as super().__init__(args) (transform_CallExpr:452-454).
Slices are Python-style, with any component omitted (to_ast.py:3014-3033):
Source
Compiles to
Evidence
a[1:4]
a[1:4]
transform_SliceExpr
a[::2]
a[::2]
transform_SliceExpr
a[i:j:k]
a[i:j:k]
transform_SliceExpr
A range used as an index is a slice, not a key (transform_IndexExpr:504-508, _range_as_slice):
Source
Compiles to
Note
a[0..<3]
a[0:3]
exclusive upper bound
a[0..3]
a[0:(3) + 1]
inclusive → stop + 1
a[1..]
a[1:]
open end
a[1..4 step 2]
a[1:(4) + 1:2]
step preserved
a[0..] → a[0:] (probe). A leading open bound (a[..3]) is not accepted: it is a parse error (probe). A multi-dimensional subscript (a[0:2, 3]) is a parse error — slices are one-dimensional (probe).
f(args) → f(args); obj.m(args) → obj.m(args) (transform_CallExpr). Klass.m(args) and a Python module/object method emit the name verbatim.
Keyword arguments use := or : in the source and render Python = (to_ast.py:3070-3084): f(1, b: 2) → f(1, b=2) (probe). A positional argument after a keyword is a parse error.
A generator expression as the sole argument is parsed inline (to_ast.py:3090-3112), so sum(x for x in xs) works without extra parens (probe).
Spreads: *items → *items; **mapping → **mapping. The marker is placed in SpreadExpr.is_dict (to_ast.py:3051-3069).
...value is adaptive (transform_CallExpr:459-464): when it is the sole argument, the call lowers to _aura_call(func, value), which unpacks a dict as keyword arguments and anything else as positional arguments (transform_CallExpr:386-394 for member calls). The helper is injected on demand.
Aura method conveniences apply to member calls when the name is not a user-declared method (known_member wins, transform_CallExpr:400-412):
An unknown member name is emitted verbatim (Python passthrough), e.g. xs.append(4) → xs.append(4), xs.pop(), xs.sort() (transform_CallExpr:441-444).
5. Arithmetic: + - * / % **
Aura follows Python arithmetic, which differs from JVM/Kof:
Expression
Result
Why
Evidence
7 / 2
3.5
/ is true division; never truncates
probe
-7 % 3
2
% follows the divisor’s sign (Python); Kof/JVM follows the dividend
probe
7 % -3
-2
same rule
probe
2 ** 3 ** 2
512
** is right-associative
probe
-2 ** 2
-4
unary - binds looser than **
probe
There is no integer floor division.// always starts a line comment (grammar.md §1.1); cast to get an integer: int(total / count).
+ - * / % render (a op b) and ** renders (a ** b) (transform_BinaryOp:290-298).
+ on two strings (or any sequence) is concatenation; * with an integer repeats ("ab" * 2 → 'ab' * 2, probe). These are Python semantics.
a + b .. c → range((a + b), c + 1): + binds tighter than .. (probe).
6. Comparison, equality, membership, identity
All of < > <= >= == != in not in is is not sit at precedence 5 and render (a op b) (transform_BinaryOp:294-295).
==/!= are value equality through Python’s == (Python semantics per type). is/is not are identity and render Python is/is not.
x is none / x is not none is the idiomatic null check → (x is None) (probe).
Identity against any other literal is a parse error (to_ast.py:508-518, 2616-2626): x is "a", x is 5, x is true, x is not "a" all raise “‘is’ compares identity; use ‘==’ (or ‘!=’) to compare with a literal” (probe). none is deliberately excluded from the literal check so x is none is accepted.
in / not in render Python in / not in. 3 in [1, 2, 3] → True (probe).
No comparison chaining: a == b == c → ((a == b) == c) (probe).
7. Logical: and or not
and/or render (a and b) / (a or b) and are left-associative (transform_BinaryOp:293, to_ast.py:470-471).
They use Python truthiness and return an operand, not a coerced bool: 1 and 2 → 2, 0 or "fallback" → "fallback" (probe).
Short-circuit: and/or do not evaluate the right operand when the left decides — inherited from the emitted Python.
not renders (not a) (transform_UnaryOp:323-327). It binds looser than comparison and bitwise, tighter than and (to_ast.py:2487-2494): not 0 → True, not not a → (not (not a)) (probe).
&&, ||, ! are removed: each is a pointed parse error telling you to use and/or/not (to_ast.py:2546-2551, 2484-2486, probe).
8. Bitwise and shifts: & | ^ ~ << >>
& | ^ render bitwise between integers; <</>> render Python shifts (transform_BinaryOp:290-298, probe: x << 2 → (x << 2)).
~ is a unary prefix in Aura and renders (~ x) (transform_UnaryOp:323-327, probe). There is no missing complement — ~x is valid.
Bitwise precedence is Python’s: | (6) looser than ^ (7) looser than & (8) looser than shifts (9), all tighter than comparison: x | y & z ^ w → (x | ((y & z) ^ w)) (probe).
There is no >>> form; >> is the only right shift.
9. Ternary ? :, Elvis ?:, null-coalescing ??
Form
Meaning
Compiles to
Evidence
c ? t : f
conditional
(t if c else f)
transform_CondExpr
a ?: b
b when a is falsy (none/false/0/""/[]/{})
_aura_elvis(a, b)
transform_BinaryOp:262-264
a ?? b
b only when a is none
_aura_null_coalesce(a, b)
transform_BinaryOp:259-261
The ?: and ?? helpers are injected on demand (uses_coalesce, transform_BinaryOp:259-264). Their bodies are value if value is not None else default and value if value else default respectively (probe).
a ?? b ?? c is right-associative; a ?: b ?: c is left-associative (§1).
??= is the compound assignment form and lowers to x = x if x is not None else y (x ??= y, probe).
Ternary binds looser than everything down to or; the branches parse at the lowest precedence (to_ast.py:2586-2593): a ?: b + c → _aura_elvis(a, (b + c)) (probe).
10. Pipe |>
a |> f applies f to a; a |> f(b, c) inserts a as the first argument (transform_PipeExpr:578-591).
Source
Compiles to
a |> f
f(a)
a |> f(b)
f(a, b)
a |> f |> g
g(f(a))
Pipe is the loosest expression operator (level 1) and chains left to right (to_ast.py:2558-2563).
When the right side is a call to a stdlib collection function (map, filter, reduce, take, drop), the transformer injects the stdlib.collections import (transform_PipeExpr, probe). The canonical pipeline is in closures.md §4.
A range evaluates to a Python range object; list(1..5) → [1, 2, 3, 4, 5] (probe).
An open-ended range (0..) needs the next token to not begin an end expression: a closer, {, ;, ,, ), ], :, =, step, a statement keyword, EOF, or a token on a later line ends it (_range_ends_here, to_ast.py:2636-2652). The transformer injects import itertools (uses_infinite_range).
a .. b + c → range(a, (b + c) + 1): + binds tighter than .. (probe).
Using a range as an index is a slice, not range(...) — see §3.
12. Safe navigation: ?. and ?[
Source
Compiles to
Evidence
a?.b
(a.b if a is not None else None)
transform_SafeNavExpr:561-575
a?[i]
(a[i] if a is not None else None)
transform_SafeNavExpr:564-566
a?.b?.c
nested conditional, one guard per step
probe
a?.b()
(a.b if a is not None else None)()
transform_CallExpr:386-394
Guarding is against is not None, not falsiness (probe).
user?.address?.city yields None when user is none (probe).
Caveat (Unspecified):a?[0..<2] is not converted to a slice — it emits (a[range(0, 2)] if a is not None else None), which raises TypeError at runtime. Only the plain a[0..<2] path (transform_IndexExpr:504-508) applies the range-as-slice rule (probe).
13. Spread: *, **, ...
Context
Form
Compiles to
Evidence
list
[*a, *b]
[*a, *b]
transform_ListLiteral
set
{*a}
{*a}
transform_SetLiteral:218-225
dict
{**a, **b}
AuraDict({**a, **b})
transform_DictLiteral:199-216
call
f(*a), f(**m)
f(*a), f(**m)
_render_call_args:479-495
call
f(...v)
_aura_call(f, v)
transform_CallExpr:456-464
*/**/... are prefix markers, valid only where a spread is allowed (call arguments and list/set/tuple/dict literals). A bare spread in a value position is a parse error (to_ast.py:2505-2515, probe: [**a] fails).
... inside a list or tuple literal behaves as positional spread (to_ast.py:3170-3171, 3206-3207).
A dotted name and a spread are the only alternatives: 1...10 is rejected with a hint to use ../..< (to_ast.py:2538-2543, probe).
Multiple for and multiple if clauses are supported and preserve source order: [x*y for x in r for y in s] → [(x * y) for x in r for y in s] (probe); [x for x in r if a if b] chains the filters.
A two-name tuple pattern over a dict inserts .items() only when the iterable is provably dict-shaped (a dict literal, an AuraDict(...)/dict(...) call, or .items()/.keys()); a list of pairs is left untouched (_needs_items, transform_ComprehensionExpr:748-754).
A comprehension’s optional leading spread ([ *a for x in xs ]) makes the element a SpreadExpr (to_ast.py:3170-3171).
Comprehensions are eager; generator expressions are lazy (Python).
A single identifier before => declares a one-parameter lambda (to_ast.py:2719-2722).
A block-bodied lambda cannot be a Python lambda (statements / return), so it is hoisted to a real function _aura_lambda_N; captured mutable locals are declared nonlocal (_nonlocal_declaration:711-731).
Parameters use the def grammar: defaults, *args, **kwargs, bare * (_render_param:649-660, _parse_lambda_params:2942-2993).
See closures.md for function types and capture semantics.
16. Collections expression surface
Collection literals and their operations. Concrete syntax is in grammar.md §6.5.
16.1 Literals
Source
Compiles to
Evidence
[1, 2, 3]
[1, 2, 3]
transform_ListLiteral:195-197
[]
[]
same
{"a": 1}
AuraDict({'a': 1})
transform_DictLiteral:199-216
{}
AuraDict({})
same
{1, 2, 3}
{1, 2, 3}
transform_SetLiteral:218-225
(), (42,), (1, "a")
(), (42,), (1, 'a')
transform_TupleLiteral:227-231
Type { x: 1 }
Type(**AuraDict({'x': 1}))
to_ast.py:3119-3136
A trailing comma is allowed in every literal: [1,] → [1], {1,} → {1} (probe).
(1) is not a tuple — it is the parenthesised expression 1; a one-element tuple needs the comma (42,) (transform_TupleLiteral:229-230).
A dict lowers through AuraDict (imported on demand). Access by .name or ["key"] both work: d.name → d.name, d["age"] → d["age"] (probe).
Struct init is sugar for Type(**AuraDict({...})) and is recognised only after a capitalised identifier (to_ast.py:3119-3136). Inside a condition or a case pattern, { opens a block/body, so struct init is suppressed (_no_struct_depth, _pattern_depth).
An empty { } in expression position parses as an empty dict literal, not an empty block (see statements.md §1).
16.2 Indexing, slicing, membership, operators
Operation
Compiles to
Evidence
xs[i] (negative ok)
xs[i]
transform_IndexExpr
xs[a:b:c]
xs[a:b:c]
transform_SliceExpr
x in xs
(x in xs)
transform_BinaryOp:295
xs + [6]
(xs + [6])
transform_BinaryOp
[0] * 3
([0] * 3)
transform_BinaryOp
Runtime results (probe): xs[-1] → last element, xs[1:4] → [2, 3, 4], xs[::2] → every other element, 3 in xs → True, xs + [6] → concatenation, [0] * 3 → [0, 0, 0].
A user-declared method named size, length, len, contains, add, … is a known_member and wins over the convenience (transform_CallExpr:400-412).
.size() etc. apply to any object with a Python len/member protocol, not just lists (e.g. dict, set, str).
17. Casts: as
x as T renders T(x) (transform_BinaryOp:284-288). The right side is parsed as a type (to_ast.py:2602-2608).
Source
Compiles to
x as int
int(x)
v as str
str(v)
x as float
float(x)
as sits at precedence 13, with *///%: 1 + x as int → (1 + int(x)) and x as int + 1 → (int(x) + 1) (probe).
18. Expression-position if, match, try, blocks
These constructs may appear where a value is expected (to_ast.py:2708-2715, transform_IfStmt:607-630, transform_MatchExpr, transform_TryExpr, transform_BlockExpr).
if c { a } else { b } in expression position → (a if c else b), using the last expression / return of each branch.
match / try in expression position are hoisted into helper functions (_aura_match_N, _aura_try_N); the case body’s tail expression becomes the returned value.
A bare { … } used as a value is hoisted into _aura_block_N(), whose last expression is returned (transform_BlockExpr:890-931).
Assignment is not a value expression: x = y produces a statement, not a value (§1; the parser emits an assignment node, not a BinaryOp).
19. Operations that are NOT expressions
throw, return, break, continue are statements (statements.md).
yield is a statement-level prefix form, not part of expression; its operand parses at the lowest precedence, so yield x + 1 yields x + 1 and yield a, b yields the tuple (a, b) (grammar.md §6.1, to_ast.py:2470-2483).
There is no ??=, &&, ||, ! as operators (they are removed spellings), no // floor division, and no >>> shift.