Skip to main content

tfparser_core/ir/
expression.rs

1//! Possibly-unresolved HCL expressions.
2//!
3//! Per [10-data-model.md § 2.3], the evaluator reduces each `Expression` to
4//! either a fully-resolved [`Value`] (via [`Expression::Literal`]) or leaves
5//! it as a symbolic node that survives intact to the exporter.
6//! [`Expression::Unresolved`] is the **only** variant that may carry a
7//! symbolic reference after evaluation; all other variants are reduced or
8//! left as partial subtrees per the propagation rules in
9//! [13-evaluator.md § 6].
10//!
11//! [10-data-model.md § 2.3]: ../../specs/10-data-model.md
12//! [13-evaluator.md § 6]: ../../specs/13-evaluator.md
13
14use std::sync::Arc;
15
16use serde::{Deserialize, Serialize};
17use typed_builder::TypedBuilder;
18
19use crate::ir::{Address, Span, Value};
20
21/// Insertion-ordered association list of attribute name → expression. Used
22/// for every HCL body that has attributes (resource bodies, provider
23/// configs, module inputs, etc.).
24pub type AttributeMap = Vec<(Arc<str>, Expression)>;
25
26/// HCL binary operators.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29#[non_exhaustive]
30pub enum BinaryOp {
31    /// `a + b`
32    Add,
33    /// `a - b`
34    Sub,
35    /// `a * b`
36    Mul,
37    /// `a / b`
38    Div,
39    /// `a % b`
40    Mod,
41    /// `a == b`
42    Eq,
43    /// `a != b`
44    Ne,
45    /// `a < b`
46    Lt,
47    /// `a <= b`
48    Le,
49    /// `a > b`
50    Gt,
51    /// `a >= b`
52    Ge,
53    /// `a && b`
54    And,
55    /// `a || b`
56    Or,
57}
58
59/// HCL unary operators.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "kebab-case")]
62#[non_exhaustive]
63pub enum UnaryOp {
64    /// `-a`
65    Neg,
66    /// `!a`
67    Not,
68}
69
70/// What an [`Expression::Unresolved`] refers to syntactically. Used by the
71/// dependency-graph phase to derive edges and by the exporter to emit the
72/// `__kind__` discriminator in canonical JSON.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "PascalCase")]
75#[non_exhaustive]
76pub enum SymbolKind {
77    /// `var.<name>`
78    Var,
79    /// `local.<name>`
80    Local,
81    /// `<type>.<name>[.<attr>...]`
82    Resource,
83    /// `data.<type>.<name>[.<attr>...]`
84    Data,
85    /// `module.<name>[.<output>]`
86    Module,
87    /// `path.module`, `path.root`, etc.
88    Path,
89    /// `each.key`, `each.value`, `count.index`
90    Iteration,
91    /// `terraform.workspace`, etc.
92    Terraform,
93    /// `dependency.<name>.outputs.<x>` (Terragrunt)
94    TerragruntDependency,
95    /// Anything we recognised as a reference but cannot place in a more
96    /// specific bucket.
97    Other,
98}
99
100/// A symbolic reference left unresolved by the evaluator.
101#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
102#[non_exhaustive]
103#[serde(rename_all = "camelCase")]
104#[builder(field_defaults(setter(into)))]
105pub struct Symbolic {
106    /// What syntactic shape this reference has.
107    pub kind: SymbolKind,
108
109    /// Verbatim source of the reference (e.g. `"var.environment"`,
110    /// `"aws_iam_role.r.arn"`).
111    pub source: Arc<str>,
112
113    /// Parsed address form when the reference resolves to a Terraform
114    /// address. `None` for `Path`, `Iteration`, `Terraform`, etc.
115    #[builder(default)]
116    pub address_hint: Option<Address>,
117
118    /// Where this reference was written.
119    pub span: Span,
120}
121
122/// An HCL function call — used both for unevaluated calls (when the
123/// evaluator cannot reduce the call, e.g. due to unresolved arguments) and
124/// as an intermediate representation during evaluation.
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
126#[non_exhaustive]
127#[serde(rename_all = "camelCase")]
128#[builder(field_defaults(setter(into)))]
129pub struct FuncCall {
130    /// Function name (e.g. `"jsonencode"`).
131    pub name: Arc<str>,
132    /// Argument expressions, in order.
133    pub args: Vec<Expression>,
134    /// Where the call appears.
135    pub span: Span,
136}
137
138/// An HCL conditional `cond ? then : else`.
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
140#[non_exhaustive]
141#[serde(rename_all = "camelCase")]
142pub struct Conditional {
143    /// Condition expression.
144    pub cond: Box<Expression>,
145    /// Branch evaluated when `cond` is true.
146    pub then_branch: Box<Expression>,
147    /// Branch evaluated when `cond` is false.
148    pub else_branch: Box<Expression>,
149    /// Span of the whole conditional.
150    pub span: Span,
151}
152
153/// An HCL `for` comprehension — captured verbatim when the evaluator cannot
154/// reduce it (typically because the source collection is unresolved).
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
156#[non_exhaustive]
157#[serde(rename_all = "camelCase")]
158#[builder(field_defaults(setter(into)))]
159pub struct ForExpr {
160    /// Iteration variable name(s): `[key, value]` if a key was bound, else
161    /// `[value]`.
162    pub binders: Vec<Arc<str>>,
163    /// Source collection.
164    pub collection: Box<Expression>,
165    /// Yielded key expression (object form only).
166    #[builder(default)]
167    pub key: Option<Box<Expression>>,
168    /// Yielded value expression.
169    pub value: Box<Expression>,
170    /// Optional `if` clause.
171    #[builder(default)]
172    pub cond: Option<Box<Expression>>,
173    /// Whether this is an object comprehension (`{...}`) vs. a list one (`[...]`).
174    #[builder(default = false)]
175    pub object_form: bool,
176    /// Span.
177    pub span: Span,
178}
179
180/// A possibly-resolved HCL expression.
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182#[non_exhaustive]
183#[serde(rename_all = "camelCase", tag = "kind", content = "node")]
184pub enum Expression {
185    /// A fully-resolved value.
186    Literal(Value),
187
188    /// A symbolic reference the evaluator could not resolve (`var.x`,
189    /// `local.y`, `data.z.w`, `aws_iam_role.r.arn`, etc.).
190    Unresolved(Symbolic),
191
192    /// Binary operation. Subtrees may themselves be `Unresolved`.
193    BinaryOp {
194        /// Operator.
195        op: BinaryOp,
196        /// Left-hand side.
197        lhs: Box<Expression>,
198        /// Right-hand side.
199        rhs: Box<Expression>,
200        /// Span of the whole operation.
201        span: Span,
202    },
203
204    /// Unary operation.
205    UnaryOp {
206        /// Operator.
207        op: UnaryOp,
208        /// Operand.
209        operand: Box<Expression>,
210        /// Span.
211        span: Span,
212    },
213
214    /// Template concatenation, e.g. `"foo-${var.x}-bar"` → parts.
215    TemplateConcat(Vec<Expression>),
216
217    /// HCL tuple / array literal whose elements are not all
218    /// [`Expression::Literal`] yet. The loader emits this when the array
219    /// contains at least one [`Expression::Unresolved`] / function call /
220    /// reference; once every element resolves, the evaluator collapses it
221    /// to [`Expression::Literal`] of [`Value::List`].
222    ///
223    /// Per [10-data-model.md § 2.3 expression-and-values lowering table]:
224    /// "tuple / object literals → recurse → [`Value::List`] / [`Value::Map`]
225    /// once children resolved at evaluator phase; during loader, kept as
226    /// expression nodes."
227    ///
228    /// [10-data-model.md § 2.3]: ../../specs/10-data-model.md
229    Array(Vec<Expression>),
230
231    /// HCL object literal whose keys or values are not yet fully resolved.
232    /// Keys are themselves [`Expression`] because HCL allows expression
233    /// keys (`{(var.kind) = "x"}`) — the IR preserves that until the
234    /// evaluator can collapse to a [`Value::Map`].
235    Object(Vec<(Expression, Expression)>),
236
237    /// Function call.
238    FuncCall(Box<FuncCall>),
239
240    /// `cond ? a : b`.
241    Conditional(Box<Conditional>),
242
243    /// `[for ... in ...]` / `{for ... in ...}`.
244    For(Box<ForExpr>),
245}
246
247impl Expression {
248    /// Returns the resolved [`Value`] if this expression is a
249    /// [`Expression::Literal`].
250    #[must_use]
251    pub fn as_literal(&self) -> Option<&Value> {
252        match self {
253            Self::Literal(v) => Some(v),
254            _ => None,
255        }
256    }
257
258    /// `true` iff every leaf in the expression tree is a [`Value`] (no
259    /// [`Symbolic`] anywhere).
260    #[must_use]
261    pub fn is_fully_resolved(&self) -> bool {
262        match self {
263            Self::Literal(_) => true,
264            Self::Unresolved(_) => false,
265            Self::BinaryOp { lhs, rhs, .. } => lhs.is_fully_resolved() && rhs.is_fully_resolved(),
266            Self::UnaryOp { operand, .. } => operand.is_fully_resolved(),
267            Self::TemplateConcat(parts) | Self::Array(parts) => {
268                parts.iter().all(Self::is_fully_resolved)
269            }
270            Self::Object(entries) => entries
271                .iter()
272                .all(|(k, v)| k.is_fully_resolved() && v.is_fully_resolved()),
273            Self::FuncCall(call) => call.args.iter().all(Self::is_fully_resolved),
274            Self::Conditional(c) => {
275                c.cond.is_fully_resolved()
276                    && c.then_branch.is_fully_resolved()
277                    && c.else_branch.is_fully_resolved()
278            }
279            Self::For(f) => {
280                f.collection.is_fully_resolved()
281                    && f.value.is_fully_resolved()
282                    && f.key.as_ref().is_none_or(|k| k.is_fully_resolved())
283                    && f.cond.as_ref().is_none_or(|c| c.is_fully_resolved())
284            }
285        }
286    }
287}
288
289#[cfg(test)]
290#[allow(
291    clippy::unwrap_used,
292    clippy::expect_used,
293    clippy::panic,
294    clippy::indexing_slicing
295)]
296mod tests {
297    use std::{path::Path, sync::Arc};
298
299    use super::*;
300
301    fn fake_span() -> Span {
302        Span::synthetic()
303    }
304
305    #[test]
306    fn test_should_classify_literal_as_resolved() {
307        let e = Expression::Literal(Value::Int(42));
308        assert!(e.is_fully_resolved());
309        assert_eq!(e.as_literal(), Some(&Value::Int(42)));
310    }
311
312    #[test]
313    fn test_should_classify_unresolved_as_not_resolved() {
314        let e = Expression::Unresolved(Symbolic {
315            kind: SymbolKind::Var,
316            source: Arc::from("var.environment"),
317            address_hint: Some(Address::new("var.environment").unwrap()),
318            span: fake_span(),
319        });
320        assert!(!e.is_fully_resolved());
321    }
322
323    #[test]
324    fn test_should_recurse_into_binary_op() {
325        let e = Expression::BinaryOp {
326            op: BinaryOp::Add,
327            lhs: Box::new(Expression::Literal(Value::Int(1))),
328            rhs: Box::new(Expression::Unresolved(Symbolic {
329                kind: SymbolKind::Local,
330                source: Arc::from("local.x"),
331                address_hint: None,
332                span: fake_span(),
333            })),
334            span: fake_span(),
335        };
336        assert!(!e.is_fully_resolved());
337    }
338
339    #[test]
340    fn test_should_serde_round_trip_expression_tree() {
341        let span = Span::new(Arc::from(Path::new("/tmp/x.tf")), 0..1, 1, 1).unwrap();
342        let e = Expression::TemplateConcat(vec![
343            Expression::Literal(Value::Str(Arc::from("prefix-"))),
344            Expression::Unresolved(Symbolic {
345                kind: SymbolKind::Var,
346                source: Arc::from("var.environment"),
347                address_hint: None,
348                span: span.clone(),
349            }),
350        ]);
351        let json = serde_json::to_string(&e).unwrap();
352        let back: Expression = serde_json::from_str(&json).unwrap();
353        assert_eq!(e, back);
354    }
355
356    #[test]
357    fn test_should_serde_round_trip_func_call() {
358        let call = FuncCall {
359            name: Arc::from("jsonencode"),
360            args: vec![Expression::Literal(Value::Str(Arc::from("hi")))],
361            span: fake_span(),
362        };
363        let e = Expression::FuncCall(Box::new(call));
364        let json = serde_json::to_string(&e).unwrap();
365        let back: Expression = serde_json::from_str(&json).unwrap();
366        assert_eq!(e, back);
367    }
368
369    #[test]
370    fn test_func_call_with_unresolved_argument_is_unresolved() {
371        let call = FuncCall {
372            name: Arc::from("jsonencode"),
373            args: vec![Expression::Unresolved(Symbolic {
374                kind: SymbolKind::Var,
375                source: Arc::from("var.x"),
376                address_hint: None,
377                span: fake_span(),
378            })],
379            span: fake_span(),
380        };
381        let e = Expression::FuncCall(Box::new(call));
382        assert!(!e.is_fully_resolved());
383    }
384
385    #[test]
386    fn test_should_serde_round_trip_conditional() {
387        let cond = Conditional {
388            cond: Box::new(Expression::Literal(Value::Bool(true))),
389            then_branch: Box::new(Expression::Literal(Value::Int(1))),
390            else_branch: Box::new(Expression::Literal(Value::Int(2))),
391            span: fake_span(),
392        };
393        let e = Expression::Conditional(Box::new(cond));
394        assert!(e.is_fully_resolved());
395        let json = serde_json::to_string(&e).unwrap();
396        let back: Expression = serde_json::from_str(&json).unwrap();
397        assert_eq!(e, back);
398    }
399
400    #[test]
401    fn test_conditional_with_unresolved_branch_is_unresolved() {
402        let cond = Conditional {
403            cond: Box::new(Expression::Literal(Value::Bool(true))),
404            then_branch: Box::new(Expression::Literal(Value::Int(1))),
405            else_branch: Box::new(Expression::Unresolved(Symbolic {
406                kind: SymbolKind::Var,
407                source: Arc::from("var.x"),
408                address_hint: None,
409                span: fake_span(),
410            })),
411            span: fake_span(),
412        };
413        let e = Expression::Conditional(Box::new(cond));
414        assert!(!e.is_fully_resolved());
415    }
416
417    #[test]
418    fn test_should_serde_round_trip_for_expr() {
419        let f = ForExpr {
420            binders: vec![Arc::from("k"), Arc::from("v")],
421            collection: Box::new(Expression::Literal(Value::List(vec![Value::Int(1)]))),
422            key: Some(Box::new(Expression::Unresolved(Symbolic {
423                kind: SymbolKind::Iteration,
424                source: Arc::from("k"),
425                address_hint: None,
426                span: fake_span(),
427            }))),
428            value: Box::new(Expression::Literal(Value::Bool(true))),
429            cond: None,
430            object_form: true,
431            span: fake_span(),
432        };
433        let e = Expression::For(Box::new(f));
434        let json = serde_json::to_string(&e).unwrap();
435        let back: Expression = serde_json::from_str(&json).unwrap();
436        assert_eq!(e, back);
437        assert!(!e.is_fully_resolved(), "Iteration ref keeps it unresolved");
438    }
439}