surrealguard_syntax/ast/expr.rs
1//! Expressions, idioms (field/graph paths), and literals.
2//!
3//! The idiom model is informed by SurrealDB's own `Part` enum (the ground
4//! truth for which path constructs exist): idioms may start from a value,
5//! indices are expressions, graph steps are mini-selections, and method
6//! calls are path parts. We model the subset analysis consumes and keep the
7//! rest explicit via `Partial`.
8
9use super::{Block, PartialNode, Spanned, Statement, TypeExpr};
10
11/// A value-producing expression.
12#[derive(Clone, Debug, PartialEq)]
13pub enum Expr {
14 /// A literal value.
15 Literal(Literal),
16 /// A field path / graph traversal, rooted in the current row context or
17 /// in a leading value (see [`IdiomPart::Start`]).
18 Idiom(Idiom),
19 /// A bare `$name` — parameter or `LET` binding reference (without `$`).
20 /// `$name.field` chains lower as `Idiom` with a `Start` part instead.
21 Param(String),
22 /// A table reference. Only produced in source/target positions
23 /// (`FROM person`, `CREATE person`): a bare identifier in expression
24 /// position is a field path, not a table.
25 Table(Spanned<String>),
26 /// A record id literal: `person:one`. The id's internal structure is
27 /// kept opaque (ids can be composite; nothing consumes their shape).
28 RecordId {
29 /// The table portion (`person` in `person:one`).
30 table: Spanned<String>,
31 /// The id portion, kept as an opaque span.
32 id: crate::span::ByteRange,
33 /// Whether the id portion is a *range* (`person:a..z`) rather than a
34 /// single id. A range denotes many records, so it carries none of the
35 /// single-row guarantee a plain record id does.
36 range: bool,
37 },
38 /// A binary operation: `a + b`, `a AND b`.
39 Binary {
40 /// Left operand.
41 lhs: Box<Spanned<Expr>>,
42 /// The operator.
43 op: Spanned<BinaryOp>,
44 /// Right operand.
45 rhs: Box<Spanned<Expr>>,
46 },
47 /// A prefix operation: `!x`, `-x`.
48 Prefix {
49 /// The operator.
50 op: Spanned<PrefixOp>,
51 /// The operand.
52 expr: Box<Spanned<Expr>>,
53 },
54 /// A function call.
55 Call(Call),
56 /// An object literal: `{ a: 1, b: 2 }`.
57 Object(Vec<(Spanned<String>, Spanned<Expr>)>),
58 /// An array literal: `[1, 2, 3]`.
59 Array(Vec<Spanned<Expr>>),
60 /// A parenthesized statement used as a value: `(SELECT ...)`.
61 Subquery(Box<Spanned<Statement>>),
62 /// A `{ ...; ... }` block used as a value.
63 Block(Block),
64 /// A type cast: `<int> $x`.
65 Cast {
66 /// The target type.
67 ty: Spanned<TypeExpr>,
68 /// The expression being cast.
69 expr: Box<Spanned<Expr>>,
70 },
71 /// A closure value.
72 Closure(Closure),
73 /// An expression that failed to lower.
74 Partial(PartialNode),
75}
76
77/// A closure value: `|$x: int| $x + 1` / `|$x| -> int { ... }`.
78#[derive(Clone, Debug, PartialEq)]
79pub struct Closure {
80 /// Parameters with their declared types, if any.
81 pub params: Vec<(Spanned<String>, Option<Spanned<TypeExpr>>)>,
82 /// The declared return type (`-> int`), if any.
83 pub return_ty: Option<Spanned<TypeExpr>>,
84 /// The body — a block lowers to [`Expr::Block`].
85 pub body: Box<Spanned<Expr>>,
86}
87
88/// A literal value. Carries only what analysis consumes: the `Int` payload
89/// feeds `LIMIT`/array-length facts; the rest matter for their *kind*.
90/// Datetime/Uuid/Regex arise from prefixed strings (`d'…'`/`u'…'`/`r'…'`),
91/// normalized during lowering.
92#[derive(Clone, Debug, PartialEq)]
93pub enum Literal {
94 /// An integer literal; its value feeds `LIMIT`/array-length facts.
95 Int(i64),
96 /// A floating-point literal.
97 Float(f64),
98 /// A `decimal` literal — only its kind matters, so no value is kept.
99 Decimal,
100 /// A string literal's contents.
101 String(String),
102 /// A boolean literal.
103 Bool(bool),
104 /// The `NONE` literal.
105 None,
106 /// The `NULL` literal.
107 Null,
108 /// A duration literal, carrying its raw text (`1w2d`).
109 Duration(String),
110 /// A datetime literal's inner text (`2024-01-01T00:00:00Z`).
111 Datetime(String),
112 /// A uuid literal's inner text.
113 Uuid(String),
114 /// A regex literal's pattern text.
115 Regex(String),
116}
117
118/// A dotted / graph path: `profile.email`, `->likes->post.{title, id}`,
119/// `$user.name`, `tags[WHERE active]`. The load-bearing type of the AST —
120/// every part is individually spanned so diagnostics can point at one arrow
121/// or one field.
122#[derive(Clone, Debug, PartialEq)]
123pub struct Idiom {
124 /// The path segments, in order; each is individually spanned.
125 pub parts: Vec<Spanned<IdiomPart>>,
126}
127
128/// One segment of an [`Idiom`] path.
129#[derive(Clone, Debug, PartialEq)]
130pub enum IdiomPart {
131 /// A leading value the rest of the path is applied to: `$user.name`,
132 /// `(SELECT ...)[0]`. Always the first part when present.
133 Start(Box<Spanned<Expr>>),
134 /// A plain field segment: `name`.
135 Field(String),
136 /// Index by expression: `tags[0]`, `tags[$i]`.
137 Index(Box<Spanned<Expr>>),
138 /// `.*` / `[*]`.
139 All,
140 /// `[$]` — last element.
141 Last,
142 /// One graph step: `->likes` / `<-likes` / `<->likes`, possibly a full
143 /// inline selection (`->(likes WHERE since > $x)`).
144 Graph {
145 /// Direction, spanned to the arrow token itself.
146 dir: Spanned<GraphDir>,
147 /// The edge selection reached by the step.
148 step: GraphStep,
149 },
150 /// Brace selection: `.{name, age}` — real sub-idioms, not comma-split text.
151 Destructure(Vec<Spanned<Idiom>>),
152 /// Inline filter: `[WHERE ...]`.
153 Where(Box<Spanned<Expr>>),
154 /// Method call as a path part: `foo.len()`.
155 Method {
156 /// The method name.
157 name: Spanned<String>,
158 /// The call arguments.
159 args: Vec<Spanned<Expr>>,
160 },
161 /// Graph recursion: `.{1..3}` / `.{..}` — `bounded` is whether an
162 /// upper bound was written.
163 Recurse {
164 /// Whether an upper recursion bound was written.
165 bounded: bool,
166 },
167 /// Optional chaining marker: `foo?.bar`.
168 Optional,
169 /// A path segment that failed to lower.
170 Partial(PartialNode),
171}
172
173/// Direction of a graph traversal step.
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175pub enum GraphDir {
176 /// `->` — outgoing edges.
177 Out,
178 /// `<-` — incoming edges.
179 In,
180 /// `<->` — edges in either direction.
181 Both,
182}
183
184/// The selection inside one graph step. SurrealDB allows a full mini-select
185/// here (multiple edge tables, WHERE, ORDER, LIMIT, alias, ...) — we model
186/// what analysis consumes (targets + filter) and bucket the rest.
187#[derive(Clone, Debug, PartialEq)]
188pub struct GraphStep {
189 /// One or more edge tables: `->likes` or `->(likes, follows)`.
190 pub targets: Vec<Spanned<String>>,
191 /// `->(likes WHERE since > $x)` / `->likes[WHERE ...]`.
192 pub where_clause: Option<Box<Spanned<Expr>>>,
193 /// The step is a record-reference traversal (`<~`), not a graph-edge
194 /// traversal (`<-`/`->`/`<->`). Reference steps follow `REFERENCE` fields
195 /// rather than relation tables, so typing resolves them differently.
196 pub reference: bool,
197}
198
199/// A function call. `path` is pre-normalized during lowering
200/// (`type::is::record` → `type::is_record`); argument expressions keep their
201/// own spans so per-argument diagnostics need no re-derivation.
202#[derive(Clone, Debug, PartialEq)]
203pub struct Call {
204 /// The canonicalized function path (`type::is_record`).
205 pub path: Spanned<String>,
206 /// The argument expressions, each retaining its own span.
207 pub args: Vec<Spanned<Expr>>,
208}
209
210/// Binary operators the analyzers understand, plus an explicit escape hatch
211/// for everything else — an unknown operator is a fact, not a guess.
212#[derive(Clone, Debug, PartialEq, Eq)]
213pub enum BinaryOp {
214 /// `+`
215 Add,
216 /// `-`
217 Sub,
218 /// `*`
219 Mul,
220 /// `/`
221 Div,
222 /// `=` / `==`
223 Eq,
224 /// `!=`
225 NotEq,
226 /// `<`
227 Lt,
228 /// `<=`
229 LtEq,
230 /// `>`
231 Gt,
232 /// `>=`
233 GtEq,
234 /// `AND` / `&&`
235 And,
236 /// `OR` / `||`
237 Or,
238 /// `??` — null coalescing.
239 NullCoalesce,
240 /// Any operator not modeled above, kept as raw text.
241 Other(String),
242}
243
244/// Prefix operators, with an explicit escape hatch for the unmodeled.
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub enum PrefixOp {
247 /// `!` — logical negation.
248 Not,
249 /// `-` — arithmetic negation.
250 Neg,
251 /// `+` — unary plus.
252 Pos,
253 /// Any operator not modeled above, kept as raw text.
254 Other(String),
255}