Skip to main content

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}