teksilo_parse/ir.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Intermediate representation for the `teksu!` DSL.
5//!
6//! The parser builds an IR tree; the lowering walks the tree and emits
7//! the final builder-call token stream. Keeping parse and lower separate
8//! keeps span handling predictable: each IR node carries the span of the
9//! user token it originated from.
10
11use proc_macro2::Span;
12use syn::{Block, Expr, Ident, Local, Pat, Path};
13
14/// The root of a `teksu!` invocation.
15pub struct TeksiRoot {
16 /// If `Some(ident)`, the macro was called as `teksu!(ident => ...)`
17 /// and expansion should wrap the root in `ident.add(...)` to return
18 /// a `WidgetId`. If `None`, expansion returns the widget value
19 /// directly.
20 pub ctx: Option<Ident>,
21 pub root: TeksiElement,
22}
23
24/// An element: `Type[::ctor](args...) { body }`.
25pub struct TeksiElement {
26 /// The full callable path the user wrote. `Button("x")` stores
27 /// `Button`; `Button::new(lit!("x"))` stores the whole
28 /// `Button::new_literal` path. Lowering appends `::new` only when
29 /// `has_explicit_ctor` is false.
30 pub type_path: Path,
31 /// True when the user named a constructor explicitly (a lowercase
32 /// last path segment, per Rust naming convention). Lowering then
33 /// calls the path as-is without appending `::new`.
34 pub has_explicit_ctor: bool,
35 /// Positional arguments between the parens after the type path.
36 /// Empty when the user wrote `VStack` with no parens (equivalent to
37 /// `VStack()`).
38 pub args: Vec<Expr>,
39 /// Body items in source order.
40 pub body: Vec<BodyItem>,
41 /// Span of the type path's first segment — used for error reporting
42 /// on constructor typos.
43 pub head_span: Span,
44 /// Span of the closing `)` after the args, if the user wrote any
45 /// args parens (including empty `()`). `None` when no arg parens
46 /// were written. Consumers (e.g. the formatter) use this to find
47 /// the exact byte offset just past the args.
48 pub args_close: Option<Span>,
49 /// Span of the closing `}` of the body, if the user wrote any body
50 /// braces (including empty `{}`). `None` when no body braces were
51 /// written. Consumers use this to find the exact end of the body
52 /// for trivia attribution.
53 pub body_close: Option<Span>,
54}
55
56/// One item in an element's body block.
57#[allow(clippy::large_enum_variant)]
58pub enum BodyItem {
59 /// `name: arg1, arg2, ...` — builder method call with N args.
60 /// A bare lowercase ident with no body is modeled as `args == []`.
61 Property(TeksiProperty),
62 /// A bare element at body position — attaches via `.child(...)`.
63 Child(TeksiElement),
64 /// A bare Rust *expression* at body position that evaluates to a
65 /// widget — the `section("Header")` / `row(x).bold()` helper-call
66 /// shape. Attaches via `.child(expr)`. Distinct from `Child`, which
67 /// is a teksu element the macro constructs itself.
68 ExprChild { expr: Expr, span: Span },
69 /// `name = Element` — a binding that hoists `let name = ctx.add(...)`
70 /// to the enclosing statement-forming block, then attaches via
71 /// `.child(name)` on the parent.
72 Binding { name: Ident, element: TeksiElement },
73 /// `#{ expr }` at body position — the expr is expected to evaluate
74 /// to a `WidgetId` and attaches via `.child(expr)`.
75 /// The semantics are simple: always WidgetId. The full
76 /// `IntoTeksiChild` routing (widget-or-id dispatch) is not yet implemented.
77 Escape { expr: Expr, span: Span },
78 /// `let pat = expr;` at body position. Introduces a
79 /// local whose value is used by subsequent body items. Triggers
80 /// statement-sequence lowering on the enclosing element.
81 Let(Local),
82 /// `rust { ... }` imperative escape. Two forms:
83 /// expression-producing (block tail has no semicolon, lowered as
84 /// `.child(block)`) and side-effect (block tail ends in `;`,
85 /// emitted inline as a side-effect statement).
86 Rust {
87 block: Block,
88 span: Span,
89 shape: RustShape,
90 },
91 /// `if cond { Element } [else if cond { Element }]* [else { Element }]?`
92 /// Lowers to `.child_opt(...)` (no-else) or
93 /// `.child(TeksiBranch{N}::...)` (with else branches).
94 If(TeksiIf),
95 /// `match expr { pat => Element, ... }`. Lowers to
96 /// `.child(match ... { ... TeksiBranch{N}::... })` with arms
97 /// dispatched by variant index.
98 Match(TeksiMatch),
99 /// `for pat in iter { Element }`. Lowers to
100 /// `.children(iter.map(|pat| Element))`.
101 For(TeksiFor),
102 /// `..expr` — inlines an iterator of `WidgetId`s as
103 /// children. Forces statement-sequence lowering on the parent.
104 Spread { expr: Expr, span: Span },
105}
106
107pub struct TeksiIf {
108 pub cond: Expr,
109 pub then: TeksiElement,
110 pub else_branch: Option<Box<TeksiElse>>,
111 pub span: Span,
112 /// Span of the `}` that closes this if's `then` block. Held by every
113 /// `TeksiIf` (including else-if recursion) so consumers can compute
114 /// the rightmost byte of an if-chain. Inner `TeksiElement`s carry no
115 /// braces of their own — the structural form owns them.
116 pub body_close: Span,
117}
118
119#[allow(clippy::large_enum_variant)]
120pub enum TeksiElse {
121 ElseIf(TeksiIf),
122 Element {
123 element: TeksiElement,
124 /// Span of the `}` that closes the trailing `else { ... }` block.
125 body_close: Span,
126 },
127}
128
129pub struct TeksiMatch {
130 pub scrutinee: Expr,
131 pub arms: Vec<TeksiMatchArm>,
132 pub span: Span,
133 /// Span of the `}` that closes the match block.
134 pub body_close: Span,
135}
136
137pub struct TeksiMatchArm {
138 pub pat: Pat,
139 pub guard: Option<(syn::Token![if], Expr)>,
140 pub element: TeksiElement,
141}
142
143pub struct TeksiFor {
144 pub pat: Pat,
145 pub iter: Expr,
146 pub lets: Vec<Local>,
147 pub element: TeksiElement,
148 pub span: Span,
149 /// Span of the `}` that closes the for block.
150 pub body_close: Span,
151}
152
153#[derive(Clone, Copy, PartialEq, Eq)]
154pub enum RustShape {
155 /// Block's last statement is an expression without trailing `;` —
156 /// the block's value becomes a child via `.child(block)`.
157 Expression,
158 /// Block has a unit tail (last statement ends in `;` or is a
159 /// `Stmt::Semi`) — emitted as a side-effect statement.
160 SideEffect,
161}
162
163pub struct TeksiProperty {
164 pub name: Ident,
165 pub args: Vec<PropArg>,
166}
167
168/// A property argument. `Expr` and `Element` both emit `.prop_name(...)`;
169/// `Escape` and `Binding` force the `_id` slot suffix per spec §A.3 and
170/// hoist the binding when present.
171pub enum PropArg {
172 /// A plain Rust expression (scalars, closures, method calls).
173 Expr(Expr),
174 /// An embedded teksu element — `tab: "name", Card { ... }`.
175 Element(TeksiElement),
176 /// `#{ expr }` — a WidgetId expression that routes to `.prop_id`.
177 Escape(Expr),
178 /// `name = Element` — hoists `let name = ctx.add(...)` and routes
179 /// to `.prop_id(name)`.
180 Binding { name: Ident, element: TeksiElement },
181}