Skip to main content

cedar_policy_core/pst/
expr.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Expression types for PST.
18//!
19//! This module defines the expression tree used in Cedar policy conditions
20//! (`when` / `unless` clauses). Expressions are recursive via [`Arc<Expr>`].
21
22use super::err::{
23    error_body::{self},
24    PstConstructionError,
25};
26use crate::ast;
27use crate::expr_builder::ExprBuilder;
28use crate::extensions::Extensions;
29use smol_str::{SmolStr, ToSmolStr};
30use std::collections::{BTreeMap, HashSet};
31use std::fmt::Display;
32use std::str::FromStr;
33use std::sync::Arc;
34
35/// Constants for core Cedar operator names
36mod constants {
37    // The operators that are defined only in syntax
38    pub static NOT_EQ_STR: &str = "!=";
39    pub static GREATER_STR: &str = ">";
40    pub static GREATER_EQ_STR: &str = ">=";
41    pub static AND_STR: &str = "&&";
42    pub static OR_STR: &str = "||";
43}
44
45/// A validated Cedar identifier.
46///
47/// Wraps a [`SmolStr`] that has been checked to be a valid Cedar identifier
48/// (not a reserved keyword, no special characters, etc.).
49///
50/// The only way to create an `Id` is through [`Id::new()`] (which validates
51/// that the input is a valid identifier) or through conversion from other
52/// validated identifier representations.
53/// Accessing the inner string is free via [`as_str()`](Id::as_str) or
54/// [`into_smolstr()`](Id::into_smolstr).
55///
56/// ```
57/// # use cedar_policy_core::pst::Id;
58/// let id = Id::new("userName").expect("valid identifier");
59/// assert_eq!(id.as_str(), "userName");
60///
61/// // Reserved keywords are rejected:
62/// assert!(Id::new("if").is_err());
63/// assert!(Id::new("true").is_err());
64/// ```
65#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
66pub struct Id(SmolStr);
67
68impl Id {
69    /// Create a new `Id`, validating that the string is a legal Cedar identifier.
70    pub fn new(s: impl AsRef<str>) -> Result<Self, PstConstructionError> {
71        let ast_id = ast::Id::from_str(s.as_ref())?;
72        Ok(Self(ast_id.into_smolstr()))
73    }
74
75    /// Get the underlying string as a `&str`. Zero-cost.
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79
80    /// Consume the `Id` and return the underlying `SmolStr`. Zero-cost.
81    pub fn into_smolstr(self) -> SmolStr {
82        self.0
83    }
84}
85
86impl AsRef<str> for Id {
87    fn as_ref(&self) -> &str {
88        &self.0
89    }
90}
91
92impl Display for Id {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", &self.0)
95    }
96}
97
98/// Infallible: `ast::Id` is already validated.
99impl From<ast::Id> for Id {
100    fn from(id: ast::Id) -> Self {
101        Id(id.into_smolstr())
102    }
103}
104
105/// Slot identifier for template policies.
106///
107/// In Cedar, template slots are placeholders written as `?principal` or `?resource`
108/// that get filled in when a template is instantiated into a concrete policy.
109///
110/// ```cedar
111/// permit (
112///   principal == ?principal,
113///   action == Action::"view",
114///   resource in ?resource
115/// );
116/// ```
117///
118/// This enum is `#[non_exhaustive]`; match arms must include a wildcard.
119#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
120#[non_exhaustive]
121pub enum SlotId {
122    /// `?principal` slot
123    Principal,
124    /// `?resource` slot
125    Resource,
126}
127
128impl Display for SlotId {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        let b: ast::SlotId = (*self).into();
131        write!(f, "{}", b)
132    }
133}
134
135/// A qualified name (e.g., `Namespace::Type`).
136///
137/// Represents entity types, action names, and other identifiers in Cedar.
138/// Names consist of a basename and optional namespace components.
139///
140/// ```cedar
141/// // Unqualified: just a basename
142/// User
143/// Photo
144///
145/// // Qualified: namespace components followed by basename
146/// MyApp::User
147/// AWS::EC2::Instance
148/// ```
149#[derive(Debug, Clone, PartialEq, Eq, Hash)]
150pub struct Name {
151    /// Basename (the final component of the name)
152    pub id: Id,
153    /// Namespace components (empty for unqualified names)
154    pub namespace: Arc<Vec<Id>>,
155}
156
157impl Name {
158    /// Constructs an unqualified name. This is a convenience constructor that validates
159    /// that `id` is a legal Cedar identifier.
160    ///
161    /// If you have an `Id` (which is `AsRef<str>`), you can infallibly construct the name
162    /// yourself.
163    pub fn unqualified(id: impl AsRef<str>) -> Result<Self, PstConstructionError> {
164        Ok(Name {
165            id: Id::new(id)?,
166            namespace: Arc::new(vec![]),
167        })
168    }
169
170    /// Constructs a qualified name. Validates that all components are legal Cedar identifiers.
171    ///
172    /// If you have an `Id` and a namespace in the form of a `Vec<Id>`, you can infallibly
173    /// construct the name yourself.
174    pub fn qualified<I, T>(namespace: I, id: impl AsRef<str>) -> Result<Self, PstConstructionError>
175    where
176        I: IntoIterator<Item = T>,
177        T: AsRef<str>,
178    {
179        let ns: Result<Vec<Id>, _> = namespace.into_iter().map(|s| Id::new(s)).collect();
180        Ok(Name {
181            id: Id::new(id)?,
182            namespace: Arc::new(ns?),
183        })
184    }
185}
186
187impl Display for Name {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        for elem in self.namespace.as_ref() {
190            write!(f, "{elem}::")?;
191        }
192        write!(f, "{}", self.id)?;
193        Ok(())
194    }
195}
196
197/// Entity type name.
198///
199/// Represents the type of an entity in Cedar.
200///
201/// ```cedar
202/// User            // unqualified
203/// MyApp::Photo    // qualified with namespace
204/// ```
205#[derive(Debug, Clone, PartialEq, Eq, Hash)]
206pub struct EntityType(pub Name);
207
208impl EntityType {
209    /// Create an entity type from a name
210    pub fn from_name(name: impl Into<Name>) -> Self {
211        EntityType(name.into())
212    }
213}
214
215impl Display for EntityType {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        let ast_et: ast::EntityType = self.clone().into();
218        write!(f, "{}", ast_et)
219    }
220}
221
222/// Entity unique identifier (UID).
223///
224/// Represents a specific entity instance in Cedar, written as `Type::"id"`.
225///
226/// ```cedar
227/// User::"alice"
228/// Photo::"vacation.jpg"
229/// MyApp::Action::"readFile"
230/// ```
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232pub struct EntityUID {
233    /// Type of the entity
234    pub ty: EntityType,
235    /// Entity identifier (EID)
236    pub eid: SmolStr,
237}
238
239impl Display for EntityUID {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        write!(f, "{}::\"{}\"", self.ty, self.eid.as_str().escape_default())
242    }
243}
244
245/// Variables available in Cedar policy expressions.
246///
247/// Cedar provides four built-in variables that refer to the authorization request:
248///
249/// ```cedar
250/// principal       // the entity making the request
251/// action          // the action being requested
252/// resource        // the entity the action targets
253/// context         // the request context record
254/// ```
255#[derive(Debug, Clone, PartialEq, Eq, Hash)]
256pub enum Var {
257    /// `principal` — the entity making the request
258    Principal,
259    /// `action` — the action being requested
260    Action,
261    /// `resource` — the entity the action targets
262    Resource,
263    /// `context` — the request context record
264    Context,
265}
266
267/// Unary operators in Cedar expressions.
268///
269/// Includes built-in operators and extension functions that take a single argument.
270///
271/// This enum is `#[non_exhaustive]`; match arms must include a wildcard.
272///
273/// ```cedar
274/// // Built-in operators
275/// !context.is_admin           // Not
276/// -(1)                        // Neg
277/// [].isEmpty()                // IsEmpty
278///
279/// // Extension constructors
280/// decimal("1.23")             // Decimal
281/// ip("10.0.0.1")              // Ip
282/// datetime("2024-01-01")      // Datetime
283/// duration("1h30m")           // Duration
284///
285/// // IP extension methods
286/// ip("10.0.0.1").isIpv4()     // IsIPv4
287/// ip("::1").isIpv6()          // IsIPV6
288/// ip("127.0.0.1").isLoopback()   // IsLoopback
289/// ip("224.0.0.1").isMulticast()  // IsMulticast
290///
291/// // Datetime extension methods
292/// datetime("2024-01-01").toDate()           // ToDate
293/// datetime("2024-01-01T12:00:00Z").toTime() // ToTime
294/// duration("1h30m").toMilliseconds()        // ToMilliseconds
295/// duration("1h30m").toSeconds()             // ToSeconds
296/// duration("1h30m").toMinutes()             // ToMinutes
297/// duration("1h30m").toHours()               // ToHours
298/// duration("30d").toDays()                  // ToDays
299/// ```
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
301#[non_exhaustive]
302pub enum UnaryOp {
303    /// `!expr`
304    Not,
305    /// `-(expr)`
306    Neg,
307    /// `expr.isEmpty()`
308    IsEmpty,
309    /// `datetime("...")`
310    Datetime,
311    /// `decimal("...")`
312    Decimal,
313    /// `duration("...")`
314    Duration,
315    /// `ip("...")`
316    Ip,
317    /// `expr.isIpv4()`
318    IsIPv4,
319    /// `expr.isIpv6()`
320    IsIPV6,
321    /// `expr.isLoopback()`
322    IsLoopback,
323    /// `expr.isMulticast()`
324    IsMulticast,
325    /// `expr.toDate()`
326    ToDate,
327    /// `expr.toTime()`
328    ToTime,
329    /// `expr.toMilliseconds()`
330    ToMilliseconds,
331    /// `expr.toSeconds()`
332    ToSeconds,
333    /// `expr.toMinutes()`
334    ToMinutes,
335    /// `expr.toHours()`
336    ToHours,
337    /// `expr.toDays()`
338    ToDays,
339}
340
341impl UnaryOp {
342    pub(crate) fn to_name(self) -> Option<&'static ast::Name> {
343        // We get the names of the extension functions from where they are defined: we don't duplicate
344        // name definitions.
345        use crate::extensions;
346        match self {
347            UnaryOp::IsEmpty | UnaryOp::Neg | UnaryOp::Not => None,
348            UnaryOp::Datetime => Some(&extensions::datetime::constants::DATETIME_CONSTRUCTOR_NAME),
349            UnaryOp::Decimal => Some(&extensions::decimal::constants::DECIMAL_FROM_STR_NAME),
350            UnaryOp::Duration => Some(&extensions::datetime::constants::DURATION_CONSTRUCTOR_NAME),
351            UnaryOp::Ip => Some(&extensions::ipaddr::names::IP_FROM_STR_NAME),
352            UnaryOp::IsIPv4 => Some(&extensions::ipaddr::names::IS_IPV4),
353            UnaryOp::IsIPV6 => Some(&extensions::ipaddr::names::IS_IPV6),
354            UnaryOp::IsLoopback => Some(&extensions::ipaddr::names::IS_LOOPBACK),
355            UnaryOp::IsMulticast => Some(&extensions::ipaddr::names::IS_MULTICAST),
356            UnaryOp::ToDate => Some(&extensions::datetime::constants::TO_DATE_NAME),
357            UnaryOp::ToTime => Some(&extensions::datetime::constants::TO_TIME_NAME),
358            UnaryOp::ToMilliseconds => Some(&extensions::datetime::constants::TO_MILLISECONDS_NAME),
359            UnaryOp::ToSeconds => Some(&extensions::datetime::constants::TO_SECONDS_NAME),
360            UnaryOp::ToMinutes => Some(&extensions::datetime::constants::TO_MINUTES_NAME),
361            UnaryOp::ToHours => Some(&extensions::datetime::constants::TO_HOURS_NAME),
362            UnaryOp::ToDays => Some(&extensions::datetime::constants::TO_DAYS_NAME),
363        }
364    }
365
366    /// Parse a unary operator from a function name
367    pub(crate) fn from_function_name(name: &str) -> Option<Self> {
368        match name {
369            "decimal" => Some(UnaryOp::Decimal),
370            "datetime" => Some(UnaryOp::Datetime),
371            "duration" => Some(UnaryOp::Duration),
372            "ip" => Some(UnaryOp::Ip),
373            "isIpv4" => Some(UnaryOp::IsIPv4),
374            "isIpv6" => Some(UnaryOp::IsIPV6),
375            "isLoopback" => Some(UnaryOp::IsLoopback),
376            "isMulticast" => Some(UnaryOp::IsMulticast),
377            "toDate" => Some(UnaryOp::ToDate),
378            "toTime" => Some(UnaryOp::ToTime),
379            "toMilliseconds" => Some(UnaryOp::ToMilliseconds),
380            "toSeconds" => Some(UnaryOp::ToSeconds),
381            "toMinutes" => Some(UnaryOp::ToMinutes),
382            "toHours" => Some(UnaryOp::ToHours),
383            "toDays" => Some(UnaryOp::ToDays),
384            _ => None,
385        }
386    }
387}
388
389impl Display for UnaryOp {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        match self {
392            UnaryOp::Not => write!(f, "{}", ast::UnaryOp::Not),
393            UnaryOp::Neg => write!(f, "{}", ast::UnaryOp::Neg),
394            UnaryOp::IsEmpty => write!(f, "{}", ast::UnaryOp::IsEmpty),
395            // Extension functions - use their name
396            _ => match self.to_name() {
397                Some(name) => write!(f, "{}", name),
398                None => write!(f, "<impossible operator>"),
399            },
400        }
401    }
402}
403
404/// Binary operators in Cedar expressions.
405///
406/// Includes built-in operators and extension functions that take two arguments.
407///
408/// This enum is `#[non_exhaustive]`; match arms must include a wildcard.
409///
410/// ```cedar
411/// // Comparison
412/// principal == User::"alice"          // Eq
413/// principal != User::"bob"            // NotEq
414/// context.age < 18                    // Less
415/// context.age <= 21                   // LessEq
416/// context.age > 13                    // Greater
417/// context.age >= 65                   // GreaterEq
418///
419/// // Logical
420/// true && false                       // And
421/// true || false                       // Or
422///
423/// // Arithmetic
424/// context.x + 1                       // Add
425/// context.x - 1                       // Sub
426/// context.x * 2                       // Mul
427///
428/// // Hierarchy / set
429/// principal in Group::"admins"        // In
430/// [1, 2, 3].contains(2)              // Contains
431/// [1, 2].containsAll([1])            // ContainsAll
432/// [1, 2].containsAny([2, 3])         // ContainsAny
433///
434/// // Tags
435/// resource.hasTag("env")              // HasTag
436/// resource.getTag("env")              // GetTag
437///
438/// // IP extension
439/// ip("10.0.0.1").isInRange(ip("10.0.0.0/24"))  // IsInRange
440///
441/// // Datetime extension
442/// datetime("2024-01-01").offset(duration("1d")) // Offset
443/// datetime("2024-01-02").durationSince(datetime("2024-01-01")) // DurationSince
444/// ```
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
446#[non_exhaustive]
447pub enum BinaryOp {
448    /// `left == right`
449    Eq,
450    /// `left != right`
451    NotEq,
452    /// `left < right`
453    Less,
454    /// `left <= right`
455    LessEq,
456    /// `left > right`
457    Greater,
458    /// `left >= right`
459    GreaterEq,
460    /// `left && right`
461    And,
462    /// `left || right`
463    Or,
464    /// `left + right`
465    Add,
466    /// `left - right`
467    Sub,
468    /// `left * right`
469    Mul,
470    /// `left in right`
471    In,
472    /// `left.contains(right)`
473    Contains,
474    /// `left.containsAll(right)`
475    ContainsAll,
476    /// `left.containsAny(right)`
477    ContainsAny,
478    /// `left.getTag(right)`
479    GetTag,
480    /// `left.hasTag(right)`
481    HasTag,
482    /// `left.isInRange(right)`
483    IsInRange,
484    /// `left.offset(right)`
485    Offset,
486    /// `left.durationSince(right)`
487    DurationSince,
488    /// `left.lessThan(right)` (decimal less than)
489    DecimalLessThan,
490    /// `left.lessThanOrEqual(right)` (decimal less than or equal)
491    DecimalLessEq,
492    /// `left.greaterThan(right)` (decimal greater than)
493    DecimalGreater,
494    /// `left.greaterThanOrEqual(right)` (decimal greater than or equal)
495    DecimalGreaterEq,
496}
497
498impl BinaryOp {
499    pub(crate) fn to_name(self) -> Option<&'static ast::Name> {
500        use crate::extensions;
501        match self {
502            BinaryOp::IsInRange => Some(&extensions::ipaddr::names::IS_IN_RANGE),
503            BinaryOp::Offset => Some(&extensions::datetime::constants::OFFSET_METHOD_NAME),
504            BinaryOp::DurationSince => Some(&extensions::datetime::constants::DURATION_SINCE_NAME),
505            BinaryOp::DecimalLessThan => Some(&extensions::decimal::constants::LESS_THAN),
506            BinaryOp::DecimalLessEq => Some(&extensions::decimal::constants::LESS_THAN_OR_EQUAL),
507            BinaryOp::DecimalGreater => Some(&extensions::decimal::constants::GREATER_THAN),
508            BinaryOp::DecimalGreaterEq => {
509                Some(&extensions::decimal::constants::GREATER_THAN_OR_EQUAL)
510            }
511            // those are operators, not names
512            BinaryOp::Eq
513            | BinaryOp::NotEq
514            | BinaryOp::And
515            | BinaryOp::Or
516            | BinaryOp::Less
517            | BinaryOp::LessEq
518            | BinaryOp::Greater
519            | BinaryOp::GreaterEq
520            | BinaryOp::Add
521            | BinaryOp::Sub
522            | BinaryOp::Mul
523            | BinaryOp::In
524            | BinaryOp::Contains
525            | BinaryOp::ContainsAll
526            | BinaryOp::ContainsAny
527            | BinaryOp::GetTag
528            | BinaryOp::HasTag => None,
529        }
530    }
531
532    /// Parse a binary operator from a function name
533    pub(crate) fn from_function_name(name: &str) -> Option<Self> {
534        match name {
535            "lessThan" => Some(BinaryOp::DecimalLessThan),
536            "lessThanOrEqual" => Some(BinaryOp::DecimalLessEq),
537            "greaterThan" => Some(BinaryOp::DecimalGreater),
538            "greaterThanOrEqual" => Some(BinaryOp::DecimalGreaterEq),
539            "isInRange" => Some(BinaryOp::IsInRange),
540            "offset" => Some(BinaryOp::Offset),
541            "durationSince" => Some(BinaryOp::DurationSince),
542            _ => None,
543        }
544    }
545}
546
547impl Display for BinaryOp {
548    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        match self {
550            BinaryOp::Eq => write!(f, "{}", ast::BinaryOp::Eq),
551            BinaryOp::NotEq => write!(f, "{}", &constants::NOT_EQ_STR),
552            BinaryOp::Less => write!(f, "{}", ast::BinaryOp::Less),
553            BinaryOp::LessEq => write!(f, "{}", ast::BinaryOp::LessEq),
554            BinaryOp::Greater => write!(f, "{}", &constants::GREATER_STR),
555            BinaryOp::GreaterEq => write!(f, "{}", &constants::GREATER_EQ_STR),
556            BinaryOp::And => write!(f, "{}", &constants::AND_STR),
557            BinaryOp::Or => write!(f, "{}", &constants::OR_STR),
558            BinaryOp::Add => write!(f, "{}", ast::BinaryOp::Add),
559            BinaryOp::Sub => write!(f, "{}", ast::BinaryOp::Sub),
560            BinaryOp::Mul => write!(f, "{}", ast::BinaryOp::Mul),
561            BinaryOp::In => write!(f, "{}", ast::BinaryOp::In),
562            BinaryOp::Contains => write!(f, "{}", ast::BinaryOp::Contains),
563            BinaryOp::ContainsAll => write!(f, "{}", ast::BinaryOp::ContainsAll),
564            BinaryOp::ContainsAny => write!(f, "{}", ast::BinaryOp::ContainsAny),
565            BinaryOp::GetTag => write!(f, "{}", ast::BinaryOp::GetTag),
566            BinaryOp::HasTag => write!(f, "{}", ast::BinaryOp::HasTag),
567            // Extension functions - use their name
568            _ => match self.to_name() {
569                Some(name) => write!(f, "{}", name),
570                None => write!(f, "<impossible operator>"),
571            },
572        }
573    }
574}
575
576/// When using the `variadic-is-in-range` feature, the `isInRange` operator
577/// can be interpreted as a variadic operator:
578/// ```cedar
579/// ip("10.0.0.1").isInRange(ip("198.51.100.0/24"), ip("203.0.113.0/24"))  // variadic IsInRange
580/// ```
581#[cfg(feature = "variadic-is-in-range")]
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
583#[non_exhaustive]
584pub enum VariadicOp {
585    /// `ip.isInRange(ip1, ip2, ...)`
586    IsInRange,
587}
588
589#[cfg(feature = "variadic-is-in-range")]
590impl VariadicOp {
591    pub(crate) fn to_name(self) -> Option<&'static ast::Name> {
592        self.to_binop().to_name()
593    }
594
595    /// Parse a binary operator from a function name
596    pub(crate) fn from_function_name(name: &str) -> Option<Self> {
597        match name {
598            "isInRange" => Some(VariadicOp::IsInRange),
599            _ => None,
600        }
601    }
602
603    /// Every variadic operator has a binary operator equivalent.
604    pub(crate) fn to_binop(self) -> BinaryOp {
605        match self {
606            VariadicOp::IsInRange => BinaryOp::IsInRange,
607        }
608    }
609}
610
611#[cfg(feature = "variadic-is-in-range")]
612impl Display for VariadicOp {
613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614        write!(f, "{}", self.to_binop())
615    }
616}
617
618/// Literal values in Cedar expressions.
619///
620/// This enum is `#[non_exhaustive]`; match arms must include a wildcard.
621///
622/// ```cedar
623/// true                    // Bool
624/// 42                      // Long
625/// "hello"                 // String
626/// User::"alice"           // EntityUID
627/// ```
628#[derive(Debug, Clone, PartialEq, Eq, Hash)]
629#[non_exhaustive]
630pub enum Literal {
631    /// `true` or `false`
632    Bool(bool),
633    /// Integer literal (e.g., `42`, `-1`)
634    Long(i64),
635    /// String literal (e.g., `"hello"`)
636    String(SmolStr),
637    /// Entity UID literal (e.g., `User::"alice"`)
638    EntityUID(EntityUID),
639}
640
641/// Pattern element for `like` expressions.
642///
643/// A pattern is a sequence of literal characters and wildcards used with the `like` operator:
644///
645/// ```cedar
646/// resource.name like "*.jpg"      // Wildcard then Char('.')...
647/// resource.name like "photo_*"    // Char('p')... then Wildcard
648/// ```
649#[derive(Debug, Clone, PartialEq, Eq, Hash)]
650pub enum PatternElem {
651    /// A literal character in the pattern
652    Char(char),
653    /// A wildcard (`*`) matching zero or more characters
654    Wildcard,
655}
656
657/// PST Expression — the core expression type for Cedar policy conditions.
658///
659/// This enum is `#[non_exhaustive]`; match arms must include a wildcard.
660///
661/// Each variant corresponds to a Cedar syntax construct. See individual variant docs
662/// for the Cedar syntax each one represents.
663#[derive(Debug, Clone, PartialEq, Eq)]
664#[non_exhaustive]
665pub enum Expr {
666    /// A literal value: `true`, `42`, `"hello"`, or `User::"alice"`.
667    Literal(Literal),
668    /// A built-in variable: `principal`, `action`, `resource`, or `context`.
669    Var(Var),
670    /// A template slot: `?principal` or `?resource`.
671    Slot(SlotId),
672    /// A unary operation.
673    ///
674    /// ```cedar
675    /// !expr           // UnaryOp::Not
676    /// -(expr)         // UnaryOp::Neg
677    /// expr.isEmpty()  // UnaryOp::IsEmpty
678    /// decimal("1.0")  // UnaryOp::Decimal
679    /// ```
680    UnaryOp {
681        /// The operator
682        op: UnaryOp,
683        /// The operand
684        expr: Arc<Expr>,
685    },
686    /// A binary operation.
687    ///
688    /// ```cedar
689    /// context.age >= 18                   // BinaryOp::GreaterEq
690    /// principal in Group::"admins"        // BinaryOp::In
691    /// [1, 2].contains(1)                 // BinaryOp::Contains
692    /// ```
693    BinaryOp {
694        /// The operator
695        op: BinaryOp,
696        /// Left operand
697        left: Arc<Expr>,
698        /// Right operand
699        right: Arc<Expr>,
700    },
701    /// Attribute access.
702    ///
703    /// ```cedar
704    /// principal.name
705    /// context.request.ip
706    /// ```
707    GetAttr {
708        /// Expression to get attribute from
709        expr: Arc<Expr>,
710        /// Attribute name
711        attr: SmolStr,
712    },
713    /// Attribute existence check. Can check nested attributes.
714    ///
715    /// ```cedar
716    /// principal has name
717    /// principal has "0notACedarIdent"
718    /// principal has address.street
719    /// ```
720    /// If there are more than one attribute, all attributes must be valid Cedar identifiers.
721    HasAttr {
722        /// Expression to check for attribute
723        expr: Arc<Expr>,
724        /// Attribute path (non-empty; multiple elements for nested checks)
725        attrs: nonempty::NonEmpty<SmolStr>,
726    },
727    /// Pattern matching with the `like` operator.
728    ///
729    /// ```cedar
730    /// resource.name like "*.jpg"
731    /// ```
732    Like {
733        /// Expression to match
734        expr: Arc<Expr>,
735        /// Pattern to match against
736        pattern: Vec<PatternElem>,
737    },
738    /// Entity type test, optionally combined with a hierarchy check.
739    ///
740    /// ```cedar
741    /// principal is User
742    /// principal is User in Group::"admins"
743    /// ```
744    Is {
745        /// Expression to test
746        expr: Arc<Expr>,
747        /// Entity type to test for
748        entity_type: EntityType,
749        /// Optional `in` hierarchy parent
750        in_expr: Option<Arc<Expr>>,
751    },
752    /// Conditional expression.
753    ///
754    /// ```cedar
755    /// if context.is_admin then "yes" else "no"
756    /// ```
757    IfThenElse {
758        /// Condition
759        cond: Arc<Expr>,
760        /// Then branch
761        then_expr: Arc<Expr>,
762        /// Else branch
763        else_expr: Arc<Expr>,
764    },
765    /// Set literal.
766    ///
767    /// ```cedar
768    /// [1, 2, 3]
769    /// [User::"alice", User::"bob"]
770    /// ```
771    Set(Vec<Arc<Expr>>),
772    /// Record literal.
773    ///
774    /// ```cedar
775    /// {"key": "value", "count": 42}
776    /// ```
777    Record(BTreeMap<String, Arc<Expr>>),
778    /// An unknown value for partial evaluation (not part of Cedar surface syntax).
779    Unknown {
780        /// Name of the unknown
781        name: SmolStr,
782    },
783    /// A TPE residual error node: indicates that this subexpression would
784    /// produce an evaluation error if reached at runtime.
785    ///
786    /// This is distinct from tolerant-ast parse errors — `ResidualError`
787    /// represents a semantically meaningful result from type-aware partial
788    /// evaluation (e.g., arithmetic overflow).
789    #[cfg(feature = "tpe")]
790    ResidualError,
791    /// A variadic extension function call (e.g., `ip.isInRange(range1, range2, ...)`).
792    #[cfg(feature = "variadic-is-in-range")]
793    VariadicOp {
794        /// The variadic operator
795        op: VariadicOp,
796        /// The first argument (receiver)
797        left: Arc<Expr>,
798        /// The remaining arguments (one or more)
799        rights: nonempty::NonEmpty<Arc<Expr>>,
800    },
801}
802
803impl Expr {
804    /// Transform a function call with arguments into a PST expression given the [`ast::Name`] of
805    /// the function. Clones the string representation of the `ast::Name` given.
806    pub(crate) fn from_function_ast_name_and_args(
807        name: &ast::Name,
808        args: Vec<Arc<Expr>>,
809    ) -> Result<Expr, PstConstructionError> {
810        Self::from_function_names_and_args(name.to_smolstr(), name, args)
811    }
812
813    /// Transform a function call with arguments into a PST expression given the [`ast::Name`] of
814    /// the function, and its [SmolStr] name.
815    /// Assumes the two names's representation as strings are equivalent, and does not clone.
816    fn from_function_names_and_args(
817        name: SmolStr,
818        ast_name: &ast::Name,
819        args: Vec<Arc<Expr>>,
820    ) -> Result<Expr, PstConstructionError> {
821        // TPE residual error nodes are represented as `error()` in the AST.
822        // Intercept them here before the extension function lookup fails.
823        #[cfg(feature = "tpe")]
824        if *ast_name == *crate::tpe::residual::ERROR_NAME {
825            return Ok(Expr::ResidualError);
826        }
827
828        let extension = Extensions::all_available().func(ast_name)?;
829
830        let expected = extension.arg_types().len();
831        let got = args.len();
832
833        #[cfg(feature = "variadic-is-in-range")]
834        let arity_ok = if extension.is_variadic() {
835            got >= expected
836        } else {
837            got == expected
838        };
839        #[cfg(not(feature = "variadic-is-in-range"))]
840        let arity_ok = got == expected;
841
842        if !arity_ok {
843            return Err(error_body::WrongArityError::new(name.into(), expected, got).into());
844        }
845        Ok(match args.len() {
846            1 => {
847                #[expect(clippy::unwrap_used, reason = "length = 1 checked in arm")]
848                let expr = args.into_iter().next().unwrap();
849                // Special case: the unknown function
850                if ast_name.to_string() == "unknown" {
851                    return Ok(Expr::Unknown {
852                        name: format!("{}", expr).into(),
853                    });
854                }
855                let op = UnaryOp::from_function_name(&ast_name.to_string())
856                    .ok_or_else(|| error_body::UnknownFunctionError::new(name.clone()))?;
857                Expr::UnaryOp { op, expr }
858            }
859            2 => {
860                let op = BinaryOp::from_function_name(&ast_name.to_string())
861                    .ok_or_else(|| error_body::UnknownFunctionError::new(name.clone()))?;
862                let mut iter = args.into_iter();
863                Expr::BinaryOp {
864                    op,
865                    #[expect(clippy::unwrap_used, reason = "length = 2 checked in match arm")]
866                    left: iter.next().unwrap(),
867                    #[expect(clippy::unwrap_used, reason = "length = 2 checked in match arm")]
868                    right: iter.next().unwrap(),
869                }
870            }
871            #[cfg(feature = "variadic-is-in-range")]
872            _ => {
873                let op = VariadicOp::from_function_name(&ast_name.to_string())
874                    .ok_or_else(|| error_body::UnknownFunctionError::new(name.clone()))?;
875                let mut iter = args.into_iter();
876                #[expect(clippy::unwrap_used, reason = "length >= 3 checked in match arm")]
877                let left = iter.next().unwrap();
878                #[expect(clippy::unwrap_used, reason = "length >= 3 checked in match arm")]
879                let first_right = iter.next().unwrap();
880                let rights = nonempty::NonEmpty {
881                    head: first_right,
882                    tail: iter.collect(),
883                };
884                Expr::VariadicOp { op, left, rights }
885            }
886            #[cfg(not(feature = "variadic-is-in-range"))]
887            _ => return Err(error_body::UnknownFunctionError::new(name).into()),
888        })
889    }
890
891    // === Expression reduction functions ===
892
893    /// Recursively accumulate a value over this expression tree.
894    ///
895    /// At each node, `f` is called first. If it returns `Some(t)`, that value is returned
896    /// immediately without recursing into children. Otherwise, the results of recursing into
897    /// all child expressions are merged pairwise with `op`. If a node has no children,
898    /// `zero` is returned.
899    pub fn reduce<T: Clone + Sized>(
900        &self,
901        f: &dyn Fn(&Self) -> Option<T>,
902        op: &dyn Fn(T, T) -> T,
903        zero: T,
904    ) -> T {
905        if let Some(t) = f(self) {
906            return t;
907        }
908        let recurse = |e: &Arc<Self>| e.reduce(f, op, zero.clone());
909        match self {
910            Expr::Literal(_) | Expr::Var(_) | Expr::Slot(_) | Expr::Unknown { .. } => zero,
911            #[cfg(feature = "tpe")]
912            Expr::ResidualError => zero,
913            Expr::UnaryOp { expr, .. }
914            | Expr::GetAttr { expr, .. }
915            | Expr::HasAttr { expr, .. }
916            | Expr::Like { expr, .. } => recurse(expr),
917            Expr::BinaryOp { left, right, .. } => op(recurse(left), recurse(right)),
918            Expr::Is { expr, in_expr, .. } => match in_expr {
919                Some(e) => op(recurse(expr), recurse(e)),
920                None => recurse(expr),
921            },
922            Expr::IfThenElse {
923                cond,
924                then_expr,
925                else_expr,
926            } => op(op(recurse(cond), recurse(then_expr)), recurse(else_expr)),
927            Expr::Set(exprs) => {
928                let mut iter = exprs.iter();
929                match iter.next() {
930                    None => zero,
931                    Some(first) => iter.fold(recurse(first), |acc, e| op(acc, recurse(e))),
932                }
933            }
934            Expr::Record(map) => {
935                let mut iter = map.values();
936                match iter.next() {
937                    None => zero,
938                    Some(first) => iter.fold(recurse(first), |acc, e| op(acc, recurse(e))),
939                }
940            }
941            #[cfg(feature = "variadic-is-in-range")]
942            Expr::VariadicOp { left, rights, .. } => rights
943                .iter()
944                .fold(recurse(left), |acc, e| op(acc, recurse(e))),
945        }
946    }
947
948    /// Does this expression contain any slots?
949    pub fn has_slots(&self) -> bool {
950        self.reduce::<bool>(
951            &|e| match e {
952                Expr::Slot(_) => Some(true),
953                _ => None,
954            },
955            &|a, b| a || b,
956            false,
957        )
958    }
959
960    /// Does this expression contain any [`Expr::Unknown`] nodes?
961    pub fn has_unknowns(&self) -> bool {
962        self.reduce::<bool>(
963            &|e| match e {
964                Expr::Unknown { .. } => Some(true),
965                _ => None,
966            },
967            &|a, b| a || b,
968            false,
969        )
970    }
971
972    /// Return the slots used in this expression
973    pub fn slots(&self) -> HashSet<SlotId> {
974        self.reduce::<HashSet<SlotId>>(
975            &|e| match e {
976                Expr::Slot(id) => Some(HashSet::from([*id])),
977                _ => None,
978            },
979            &|a, b| a.union(&b).copied().collect(),
980            HashSet::new(),
981        )
982    }
983
984    /// Does this expression contain any [`Expr::ResidualError`] nodes?
985    ///
986    /// Returns `true` if any subexpression represents a statically-known
987    /// evaluation error (as determined by TPE). Note that this says nothing
988    /// about whether the expression will, or even can, evaluate to an error.
989    /// This function should only be used to decide if the expression can be
990    /// represented as valid Cedar policy text, since the error residual variant
991    /// does not exist in the Cedar policy syntax.
992    #[cfg(feature = "tpe")]
993    pub fn has_error(&self) -> bool {
994        self.reduce::<bool>(
995            &|e| match e {
996                Expr::ResidualError => Some(true),
997                _ => None,
998            },
999            &|a, b| a || b,
1000            false,
1001        )
1002    }
1003}
1004
1005/// Builder to construct a PST [`Expr`] that implements the [`ExprBuilder`] interface. Unlike the
1006/// expression building functions, this does not perform any validation on the input and is meant
1007/// to be used internally.
1008#[derive(Clone, Debug)]
1009pub(crate) struct PstBuilder;
1010
1011impl ExprBuilder for PstBuilder {
1012    type Expr = Expr;
1013    type Data = ();
1014    type BuildError = PstConstructionError;
1015
1016    #[cfg(feature = "tolerant-ast")]
1017    type ErrorType = crate::parser::err::ParseErrors;
1018
1019    fn with_data(_data: Self::Data) -> Self {
1020        Self
1021    }
1022
1023    fn with_maybe_source_loc(self, _: Option<&crate::parser::Loc>) -> Self {
1024        // PST doesn't store source locations
1025        self
1026    }
1027
1028    fn loc(&self) -> Option<&crate::parser::Loc> {
1029        None
1030    }
1031
1032    fn data(&self) -> &Self::Data {
1033        &()
1034    }
1035
1036    fn val(self, lit: impl Into<ast::Literal>) -> Expr {
1037        Expr::Literal(From::<ast::Literal>::from(lit.into()))
1038    }
1039
1040    fn var(self, var: ast::Var) -> Expr {
1041        Expr::Var(var.into())
1042    }
1043
1044    fn unknown(self, u: ast::Unknown) -> Expr {
1045        Expr::Unknown { name: u.name }
1046    }
1047
1048    fn slot(self, s: ast::SlotId) -> Expr {
1049        Expr::Slot(s.into())
1050    }
1051
1052    fn ite_arc(self, cond: Arc<Expr>, then_expr: Arc<Expr>, else_expr: Arc<Expr>) -> Expr {
1053        Expr::IfThenElse {
1054            cond,
1055            then_expr,
1056            else_expr,
1057        }
1058    }
1059
1060    fn not(self, e: Expr) -> Expr {
1061        Expr::UnaryOp {
1062            op: UnaryOp::Not,
1063            expr: Arc::new(e),
1064        }
1065    }
1066
1067    fn is_eq(self, e1: Expr, e2: Expr) -> Expr {
1068        Expr::BinaryOp {
1069            op: BinaryOp::Eq,
1070            left: Arc::new(e1),
1071            right: Arc::new(e2),
1072        }
1073    }
1074
1075    fn noteq(self, e1: Expr, e2: Expr) -> Expr {
1076        Expr::BinaryOp {
1077            op: BinaryOp::NotEq,
1078            left: Arc::new(e1),
1079            right: Arc::new(e2),
1080        }
1081    }
1082
1083    fn and(self, e1: Expr, e2: Expr) -> Expr {
1084        Expr::BinaryOp {
1085            op: BinaryOp::And,
1086            left: Arc::new(e1),
1087            right: Arc::new(e2),
1088        }
1089    }
1090
1091    fn or(self, e1: Expr, e2: Expr) -> Expr {
1092        Expr::BinaryOp {
1093            op: BinaryOp::Or,
1094            left: Arc::new(e1),
1095            right: Arc::new(e2),
1096        }
1097    }
1098
1099    fn less(self, e1: Expr, e2: Expr) -> Expr {
1100        Expr::BinaryOp {
1101            op: BinaryOp::Less,
1102            left: Arc::new(e1),
1103            right: Arc::new(e2),
1104        }
1105    }
1106
1107    fn lesseq(self, e1: Expr, e2: Expr) -> Expr {
1108        Expr::BinaryOp {
1109            op: BinaryOp::LessEq,
1110            left: Arc::new(e1),
1111            right: Arc::new(e2),
1112        }
1113    }
1114
1115    fn greater(self, e1: Expr, e2: Expr) -> Expr {
1116        Expr::BinaryOp {
1117            op: BinaryOp::Greater,
1118            left: Arc::new(e1),
1119            right: Arc::new(e2),
1120        }
1121    }
1122
1123    fn greatereq(self, e1: Expr, e2: Expr) -> Expr {
1124        Expr::BinaryOp {
1125            op: BinaryOp::GreaterEq,
1126            left: Arc::new(e1),
1127            right: Arc::new(e2),
1128        }
1129    }
1130
1131    fn add(self, e1: Expr, e2: Expr) -> Expr {
1132        Expr::BinaryOp {
1133            op: BinaryOp::Add,
1134            left: Arc::new(e1),
1135            right: Arc::new(e2),
1136        }
1137    }
1138
1139    fn sub(self, e1: Expr, e2: Expr) -> Expr {
1140        Expr::BinaryOp {
1141            op: BinaryOp::Sub,
1142            left: Arc::new(e1),
1143            right: Arc::new(e2),
1144        }
1145    }
1146
1147    fn mul(self, e1: Expr, e2: Expr) -> Expr {
1148        Expr::BinaryOp {
1149            op: BinaryOp::Mul,
1150            left: Arc::new(e1),
1151            right: Arc::new(e2),
1152        }
1153    }
1154
1155    fn neg(self, e: Expr) -> Expr {
1156        Expr::UnaryOp {
1157            op: UnaryOp::Neg,
1158            expr: Arc::new(e),
1159        }
1160    }
1161
1162    fn is_in_arc(self, left: Arc<Expr>, right: Arc<Expr>) -> Expr {
1163        Expr::BinaryOp {
1164            op: BinaryOp::In,
1165            left,
1166            right,
1167        }
1168    }
1169
1170    fn contains(self, e1: Expr, e2: Expr) -> Expr {
1171        Expr::BinaryOp {
1172            op: BinaryOp::Contains,
1173            left: Arc::new(e1),
1174            right: Arc::new(e2),
1175        }
1176    }
1177
1178    fn contains_all(self, e1: Expr, e2: Expr) -> Expr {
1179        Expr::BinaryOp {
1180            op: BinaryOp::ContainsAll,
1181            left: Arc::new(e1),
1182            right: Arc::new(e2),
1183        }
1184    }
1185
1186    fn contains_any(self, e1: Expr, e2: Expr) -> Expr {
1187        Expr::BinaryOp {
1188            op: BinaryOp::ContainsAny,
1189            left: Arc::new(e1),
1190            right: Arc::new(e2),
1191        }
1192    }
1193
1194    fn is_empty(self, expr: Expr) -> Expr {
1195        Expr::UnaryOp {
1196            op: UnaryOp::IsEmpty,
1197            expr: Arc::new(expr),
1198        }
1199    }
1200
1201    fn get_tag(self, expr: Expr, tag: Expr) -> Expr {
1202        Expr::BinaryOp {
1203            op: BinaryOp::GetTag,
1204            left: Arc::new(expr),
1205            right: Arc::new(tag),
1206        }
1207    }
1208
1209    fn has_tag(self, expr: Expr, tag: Expr) -> Expr {
1210        Expr::BinaryOp {
1211            op: BinaryOp::HasTag,
1212            left: Arc::new(expr),
1213            right: Arc::new(tag),
1214        }
1215    }
1216
1217    fn set(self, exprs: impl IntoIterator<Item = Expr>) -> Expr {
1218        Expr::Set(exprs.into_iter().map(Arc::new).collect())
1219    }
1220
1221    fn record(
1222        self,
1223        pairs: impl IntoIterator<Item = (SmolStr, Expr)>,
1224    ) -> Result<Expr, ast::ExpressionConstructionError> {
1225        let mut map = BTreeMap::new();
1226        for (k, v) in pairs {
1227            if map.insert(k.to_string(), Arc::new(v)).is_some() {
1228                return Err(ast::expression_construction_errors::DuplicateKeyError {
1229                    key: k,
1230                    context: "in record literal",
1231                }
1232                .into());
1233            }
1234        }
1235        Ok(Expr::Record(map))
1236    }
1237
1238    fn call_extension_fn(
1239        self,
1240        fn_name: ast::Name,
1241        args: impl IntoIterator<Item = Expr>,
1242    ) -> Result<Expr, PstConstructionError> {
1243        Expr::from_function_ast_name_and_args(&fn_name, args.into_iter().map(Arc::new).collect())
1244    }
1245
1246    fn get_attr_arc(self, expr: Arc<Expr>, attr: SmolStr) -> Expr {
1247        Expr::GetAttr { expr, attr }
1248    }
1249
1250    fn has_attr_arc(self, expr: Arc<Expr>, attr: SmolStr) -> Expr {
1251        Expr::HasAttr {
1252            expr,
1253            attrs: nonempty::nonempty![attr],
1254        }
1255    }
1256
1257    fn extended_has_attr_arc(self, expr: Arc<Expr>, attrs: nonempty::NonEmpty<SmolStr>) -> Expr {
1258        Expr::HasAttr { expr, attrs }
1259    }
1260
1261    fn like(self, expr: Expr, pattern: ast::Pattern) -> Expr {
1262        Expr::Like {
1263            expr: Arc::new(expr),
1264            pattern: pattern.into(),
1265        }
1266    }
1267
1268    fn is_entity_type_arc(self, expr: Arc<Expr>, entity_type: ast::EntityType) -> Expr {
1269        Expr::Is {
1270            expr,
1271            entity_type: entity_type.into(),
1272            in_expr: None,
1273        }
1274    }
1275
1276    fn is_in_entity_type(self, e1: Expr, entity_type: ast::EntityType, e2: Expr) -> Expr {
1277        Expr::Is {
1278            expr: Arc::new(e1),
1279            entity_type: entity_type.into(),
1280            in_expr: Some(Arc::new(e2)),
1281        }
1282    }
1283
1284    #[cfg(feature = "tolerant-ast")]
1285    fn error(
1286        self,
1287        parse_errors: crate::parser::err::ParseErrors,
1288    ) -> Result<Self::Expr, Self::ErrorType> {
1289        // PST doesn't support error nodes for now, it will propagate parse errors
1290        Err(parse_errors)
1291    }
1292}
1293
1294impl std::fmt::Display for Expr {
1295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1296        let est: crate::est::Expr = self.clone().into();
1297        write!(f, "{est}")
1298    }
1299}
1300
1301#[expect(
1302    clippy::fallible_impl_from,
1303    reason = "AST records cannot have duplicate keys, so builder.record() cannot fail"
1304)]
1305#[cfg(test)]
1306mod tests {
1307    use cool_asserts::{assert_matches, assertion_failure};
1308
1309    use super::*;
1310    use std::str::FromStr;
1311
1312    // --- Id tests ---
1313
1314    #[test]
1315    fn test_id_valid_identifiers() {
1316        // Simple identifiers
1317        assert!(Id::new("x").is_ok());
1318        assert!(Id::new("userName").is_ok());
1319        assert!(Id::new("_private").is_ok());
1320        assert!(Id::new("a1").is_ok());
1321        assert!(Id::new("ABC").is_ok());
1322    }
1323
1324    #[test]
1325    fn test_id_reserved_keywords_rejected() {
1326        for kw in [
1327            "if", "then", "else", "true", "false", "in", "is", "like", "has",
1328        ] {
1329            assert!(Id::new(kw).is_err(), "keyword `{kw}` should be rejected");
1330        }
1331    }
1332
1333    #[test]
1334    fn test_id_invalid_strings_rejected() {
1335        assert!(Id::new("").is_err());
1336        assert!(Id::new("1abc").is_err()); // starts with digit
1337        assert!(Id::new("a b").is_err()); // space
1338        assert!(Id::new("a+b").is_err()); // special char
1339        assert!(Id::new("::").is_err());
1340    }
1341
1342    #[test]
1343    fn test_id_accessors() {
1344        let id = Id::new("hello").unwrap();
1345        assert_eq!(id.as_str(), "hello");
1346        assert_eq!(id.as_ref(), "hello");
1347        assert_eq!(id.to_string(), "hello");
1348        assert_eq!(id.clone().into_smolstr(), SmolStr::from("hello"));
1349    }
1350
1351    #[test]
1352    fn test_id_equality_and_ordering() {
1353        let a = Id::new("aaa").unwrap();
1354        let b = Id::new("bbb").unwrap();
1355        let a2 = Id::new("aaa").unwrap();
1356        assert_eq!(a, a2);
1357        assert_ne!(a, b);
1358        assert!(a < b);
1359    }
1360
1361    #[test]
1362    fn test_id_from_ast_id() {
1363        let ast_id = crate::ast::Id::from_str("myIdent").unwrap();
1364        let pst_id = Id::from(ast_id);
1365        assert_eq!(pst_id.as_str(), "myIdent");
1366    }
1367
1368    // --- Name tests ---
1369
1370    #[test]
1371    fn test_name_unqualified() {
1372        let name = Name::unqualified("User").unwrap();
1373        assert_eq!(name.id.as_str(), "User");
1374        assert!(name.namespace.is_empty());
1375        assert_eq!(name.to_string(), "User");
1376    }
1377
1378    #[test]
1379    fn test_name_qualified() {
1380        let name = Name::qualified(["MyApp", "Auth"], "User").unwrap();
1381        assert_eq!(name.id.as_str(), "User");
1382        assert_eq!(name.namespace.len(), 2);
1383        assert_eq!(name.namespace[0].as_str(), "MyApp");
1384        assert_eq!(name.namespace[1].as_str(), "Auth");
1385        assert_eq!(name.to_string(), "MyApp::Auth::User");
1386    }
1387
1388    #[test]
1389    fn test_name_rejects_invalid_basename() {
1390        assert!(Name::unqualified("if").is_err());
1391        assert!(Name::unqualified("1bad").is_err());
1392        assert!(Name::qualified(["Good"], "if").is_err());
1393    }
1394
1395    #[test]
1396    fn test_name_rejects_invalid_namespace_component() {
1397        assert!(Name::qualified(["true"], "User").is_err());
1398        assert!(Name::qualified(["ok", "1bad"], "User").is_err());
1399    }
1400
1401    #[test]
1402    fn test_name_roundtrip_through_ast() {
1403        let pst_name = Name::qualified(["NS"], "Foo").unwrap();
1404        let ast_name: crate::ast::Name = pst_name.clone().into();
1405        let back: Name = ast_name.into();
1406        assert_eq!(pst_name, back);
1407    }
1408
1409    // --- EntityType / EntityUID with validated names ---
1410
1411    #[test]
1412    fn test_entity_type_display_with_valid_name() {
1413        let et = EntityType::from_name(Name::unqualified("User").unwrap());
1414        assert_eq!(et.to_string(), "User");
1415        let et = EntityType::from_name(Name::qualified(["App"], "Photo").unwrap());
1416        assert_eq!(et.to_string(), "App::Photo");
1417    }
1418
1419    #[test]
1420    fn test_entity_uid_roundtrip_through_ast() {
1421        let uid = EntityUID {
1422            ty: EntityType::from_name(Name::qualified(["NS"], "Type").unwrap()),
1423            eid: SmolStr::from("eid123"),
1424        };
1425        let ast_uid: crate::ast::EntityUID = uid.clone().into();
1426        let back: EntityUID = ast_uid.into();
1427        assert_eq!(uid, back);
1428    }
1429
1430    #[test]
1431    fn test_has_slots() {
1432        // Leaf with no slot
1433        assert!(!Expr::Literal(Literal::Long(1)).has_slots());
1434        // Var has no slot
1435        assert!(!Expr::Var(Var::Principal).has_slots());
1436        // Slot itself
1437        assert!(Expr::Slot(SlotId::Principal).has_slots());
1438        assert!(Expr::Slot(SlotId::Resource).has_slots());
1439        // Slot nested inside a BinaryOp
1440        let slot = Arc::new(Expr::Slot(SlotId::Principal));
1441        let lit = Arc::new(Expr::Literal(Literal::Long(42)));
1442        let binop = Expr::BinaryOp {
1443            op: BinaryOp::Eq,
1444            left: slot,
1445            right: lit.clone(),
1446        };
1447        assert!(binop.has_slots());
1448        // BinaryOp with no slots
1449        let binop_no_slot = Expr::BinaryOp {
1450            op: BinaryOp::Eq,
1451            left: lit.clone(),
1452            right: lit.clone(),
1453        };
1454        assert!(!binop_no_slot.has_slots());
1455        // Slot nested inside a Set
1456        let set_with_slot = Expr::Set(vec![lit.clone(), Arc::new(Expr::Slot(SlotId::Resource))]);
1457        assert!(set_with_slot.has_slots());
1458        // Empty set
1459        assert!(!Expr::Set(vec![]).has_slots());
1460        // IfThenElse with slot in else branch
1461        let ite = Expr::IfThenElse {
1462            cond: lit.clone(),
1463            then_expr: lit.clone(),
1464            else_expr: Arc::new(Expr::Slot(SlotId::Principal)),
1465        };
1466        assert!(ite.has_slots());
1467        // VariadicOp with slot in rights
1468        #[cfg(feature = "variadic-is-in-range")]
1469        {
1470            let variadic_no_slot = Expr::VariadicOp {
1471                op: super::VariadicOp::IsInRange,
1472                left: lit.clone(),
1473                rights: nonempty::nonempty![lit.clone(), lit.clone()],
1474            };
1475            assert!(!variadic_no_slot.has_slots());
1476            let variadic_with_slot = Expr::VariadicOp {
1477                op: super::VariadicOp::IsInRange,
1478                left: lit.clone(),
1479                rights: nonempty::nonempty![Arc::new(Expr::Slot(SlotId::Principal))],
1480            };
1481            assert!(variadic_with_slot.has_slots());
1482        }
1483    }
1484
1485    #[test]
1486    fn test_from_function_unknown_function() {
1487        let name = ast::Name::parse_unqualified_name("unknownFunc").unwrap();
1488        let args = vec![Arc::new(Expr::Literal(Literal::Long(1)))];
1489
1490        let result = Expr::from_function_ast_name_and_args(&name, args);
1491        assert!(matches!(
1492            result,
1493            Err(PstConstructionError::UnknownFunction(..))
1494        ));
1495    }
1496
1497    #[test]
1498    fn test_from_function_wrong_arity() {
1499        let name = ast::Name::parse_unqualified_name("decimal").unwrap();
1500        let args = vec![
1501            Arc::new(Expr::Literal(Literal::Long(1))),
1502            Arc::new(Expr::Literal(Literal::Long(2))),
1503        ];
1504
1505        let result = Expr::from_function_ast_name_and_args(&name, args);
1506        assert_matches!(result, Err(PstConstructionError::WrongArity(..)));
1507    }
1508
1509    #[test]
1510    fn test_all_extension_functions_are_supported() {
1511        // This test ensures that all extension functions defined in Extensions
1512        // are properly mapped to PST operators (UnaryOp or BinaryOp)
1513        let extensions = Extensions::all_available();
1514
1515        for func in extensions.all_funcs() {
1516            let name = func.name().clone();
1517            let arity = func.arg_types().len();
1518
1519            // Create dummy "0" arguments based on arity, we don't typecheck here
1520            let args: Vec<Arc<Expr>> = (0..arity)
1521                .map(|_| Arc::new(Expr::Literal(Literal::Long(0))))
1522                .collect();
1523
1524            let result = Expr::from_function_ast_name_and_args(&name, args);
1525            assert!(
1526                result.is_ok(),
1527                "Function {} should be supported but got error: {:?}",
1528                name,
1529                result.err()
1530            );
1531            let actual = result.unwrap();
1532            print!("Expression: {}", actual);
1533            match arity {
1534                1 => {
1535                    if &name.to_string() == "unknown" {
1536                        assert!(
1537                            matches!(actual, Expr::Unknown { .. }),
1538                            "Expected unary unknown function to be Unknown expr",
1539                        );
1540                    } else {
1541                        match actual {
1542                            Expr::UnaryOp { op, .. } => {
1543                                let op_name = op.to_name();
1544                                assert!(
1545                                    op_name.is_some(),
1546                                    "UnaryOp from extension {} should have known ast::Name",
1547                                    name
1548                                );
1549                                assert_eq!(
1550                                    UnaryOp::from_function_name(&name.as_ref().to_string()),
1551                                    Some(op)
1552                                );
1553                            }
1554                            _ => {
1555                                assertion_failure!("Unary function  should produce BinaryOp", name:name)
1556                            }
1557                        }
1558                    }
1559                }
1560                2 => match actual {
1561                    Expr::BinaryOp { op, .. } => {
1562                        let op_name = op.to_name();
1563                        assert!(
1564                            op_name.is_some(),
1565                            "BinaryOp from extension {} should have known ast::Name",
1566                            name
1567                        );
1568                        assert_eq!(
1569                            BinaryOp::from_function_name(&name.as_ref().to_string()),
1570                            Some(op)
1571                        );
1572                    }
1573                    _ => assertion_failure!("Binary function  should produce BinaryOp", name:name),
1574                },
1575                _ => (),
1576            }
1577        }
1578    }
1579
1580    #[test]
1581    fn test_expr_construction_error_display() {
1582        let err: PstConstructionError =
1583            error_body::UnknownFunctionError::new("foo".to_smolstr()).into();
1584        assert!(err.to_string().contains("foo"));
1585
1586        let err: PstConstructionError =
1587            error_body::WrongArityError::new("bar".to_string(), 2, 1).into();
1588        assert!(err.to_string().contains("bar"));
1589        assert!(err.to_string().contains("2"));
1590        assert!(err.to_string().contains("1"));
1591    }
1592
1593    #[test]
1594    fn test_builder_additional_methods() {
1595        // Test unknown
1596        let expr = PstBuilder::new().unknown(ast::Unknown::new_untyped("test"));
1597        assert_matches!(expr, Expr::Unknown { .. });
1598
1599        // Test like
1600        let base = PstBuilder::new().val("test");
1601        let pattern = ast::Pattern::from(vec![ast::PatternElem::Char('a')]);
1602        let expr = PstBuilder::new().like(base, pattern);
1603        assert_matches!(expr, Expr::Like { .. });
1604
1605        // Test is_in_entity_type
1606        let base = PstBuilder::new().var(ast::Var::Principal);
1607        let entity_type = EntityType::from_name(ast::Name::parse_unqualified_name("User").unwrap());
1608        let uid = ast::EntityUID::from_components(
1609            ast::EntityType::from(ast::Name::parse_unqualified_name("User").unwrap()),
1610            ast::Eid::new("alice"),
1611            None,
1612        );
1613        let in_expr = PstBuilder::new().val(uid);
1614        let expr = PstBuilder::new().is_in_entity_type(
1615            base,
1616            entity_type.clone().try_into().unwrap(),
1617            in_expr,
1618        );
1619        if let Expr::Is {
1620            entity_type: et,
1621            in_expr: Some(_),
1622            ..
1623        } = expr
1624        {
1625            assert_eq!(et, entity_type);
1626        } else {
1627            panic!("Expected Is with in_expr");
1628        }
1629    }
1630
1631    #[test]
1632    fn test_builder_record_duplicate_keys() {
1633        let pairs = vec![
1634            (SmolStr::new("key"), PstBuilder::new().val(1i64)),
1635            (SmolStr::new("key"), PstBuilder::new().val(2i64)),
1636        ];
1637        let result = PstBuilder::new().record(pairs);
1638        assert!(matches!(
1639            result,
1640            Err(ast::ExpressionConstructionError::DuplicateKey { .. })
1641        ));
1642    }
1643
1644    mod display_tests {
1645        use super::*;
1646        use smol_str::SmolStr;
1647
1648        #[test]
1649        fn invalid_name_rejected_at_construction() {
1650            let name = "!__Cedar!";
1651            assert!(Name::unqualified(name).is_err());
1652        }
1653
1654        // NOTE: These tests verify Display output for expressions constructed via the
1655        // ExprBuilder trait (internal builder). Some operators are desugared during
1656        // construction (e.g., != becomes !(==), > becomes !(<=), && and || may become
1657        // if-then-else in AST but remain as BinaryOp in PST).
1658        //
1659        // Once a public expression builder API is implemented that constructs PST
1660        // directly without desugaring, Display will show all operators in their
1661        // original form (!=, >, >=, &&, ||, etc.).
1662
1663        fn builder() -> PstBuilder {
1664            PstBuilder::new()
1665        }
1666
1667        #[test]
1668        fn test_builder_display() {
1669            let cases = vec![
1670                // Literals
1671                (builder().val(true), "true"),
1672                (builder().val(false), "false"),
1673                (builder().val(42i64), "42"),
1674                (builder().val(-123i64), "(-123)"),
1675                (builder().val("hello"), "\"hello\""),
1676                (
1677                    builder().val(ast::EntityUID::from_components(
1678                        ast::Name::from_str("Photo").unwrap().into(),
1679                        ast::Eid::new("abc123"),
1680                        None,
1681                    )),
1682                    "Photo::\"abc123\"",
1683                ),
1684                // Variables
1685                (builder().var(ast::Var::Principal), "principal"),
1686                (builder().var(ast::Var::Action), "action"),
1687                (builder().var(ast::Var::Resource), "resource"),
1688                (builder().var(ast::Var::Context), "context"),
1689                // Slots
1690                (builder().slot(ast::SlotId::principal()), "?principal"),
1691                (builder().slot(ast::SlotId::resource()), "?resource"),
1692                // Basic unary ops
1693                (builder().not(builder().val(true)), "!true"),
1694                (builder().neg(builder().val(42i64)), "-(42)"),
1695                // Binary ops - comparison
1696                (
1697                    builder().is_eq(builder().val(1i64), builder().val(2i64)),
1698                    "1 == 2",
1699                ),
1700                (
1701                    builder().noteq(builder().val(1i64), builder().val(2i64)),
1702                    "1 != 2",
1703                ),
1704                (
1705                    builder().less(builder().val(1i64), builder().val(2i64)),
1706                    "1 < 2",
1707                ),
1708                (
1709                    builder().lesseq(builder().val(1i64), builder().val(2i64)),
1710                    "1 <= 2",
1711                ),
1712                (
1713                    builder().greater(builder().val(1i64), builder().val(2i64)),
1714                    "1 > 2",
1715                ),
1716                (
1717                    builder().greatereq(builder().val(1i64), builder().val(2i64)),
1718                    "1 >= 2",
1719                ),
1720                // Binary ops - logical
1721                (
1722                    builder().and(builder().val(true), builder().val(false)),
1723                    "true && false",
1724                ),
1725                (
1726                    builder().or(builder().val(true), builder().val(false)),
1727                    "true || false",
1728                ),
1729                // Binary ops - arithmetic
1730                (
1731                    builder().add(builder().val(1i64), builder().val(2i64)),
1732                    "1 + 2",
1733                ),
1734                (
1735                    builder().sub(builder().val(5i64), builder().val(3i64)),
1736                    "5 - 3",
1737                ),
1738                (
1739                    builder().mul(builder().val(2i64), builder().val(3i64)),
1740                    "2 * 3",
1741                ),
1742                // Binary ops - set/hierarchy
1743                (
1744                    builder().is_in(
1745                        builder().var(ast::Var::Principal),
1746                        builder().var(ast::Var::Resource),
1747                    ),
1748                    "principal in resource",
1749                ),
1750                (
1751                    builder().contains(builder().set([builder().val(1i64)]), builder().val(1i64)),
1752                    "[1].contains(1)",
1753                ),
1754                (
1755                    builder().contains_all(
1756                        builder().set([builder().val(1i64)]),
1757                        builder().set([builder().val(1i64)]),
1758                    ),
1759                    "[1].containsAll([1])",
1760                ),
1761                (
1762                    builder().contains_any(
1763                        builder().set([builder().val(1i64)]),
1764                        builder().set([builder().val(1i64)]),
1765                    ),
1766                    "[1].containsAny([1])",
1767                ),
1768                // Attribute access
1769                (
1770                    builder().get_attr(builder().var(ast::Var::Principal), SmolStr::from("name")),
1771                    "principal.name",
1772                ),
1773                (
1774                    builder().has_attr(builder().var(ast::Var::Principal), SmolStr::from("name")),
1775                    "principal has name",
1776                ),
1777                (
1778                    builder().is_entity_type(
1779                        builder().var(ast::Var::Resource),
1780                        ast::Name::from_str("Photo").unwrap().into(),
1781                    ),
1782                    "resource is Photo",
1783                ),
1784                // If-then-else
1785                (
1786                    builder().ite(
1787                        builder().val(true),
1788                        builder().val(1i64),
1789                        builder().val(2i64),
1790                    ),
1791                    "if true then 1 else 2",
1792                ),
1793                // Sets
1794                (builder().set([]), "[]"),
1795                (builder().set([builder().val(1i64)]), "[1]"),
1796                (
1797                    builder().set([
1798                        builder().val(1i64),
1799                        builder().val(2i64),
1800                        builder().val(3i64),
1801                    ]),
1802                    "[1, 2, 3]",
1803                ),
1804                // Records
1805                (builder().record([]).unwrap(), "{}"),
1806                (
1807                    builder()
1808                        .record([(SmolStr::from("a"), builder().val(1i64))])
1809                        .unwrap(),
1810                    "{a: 1}",
1811                ),
1812                (
1813                    builder()
1814                        .record([
1815                            (SmolStr::from("a"), builder().val(1i64)),
1816                            (SmolStr::from("b"), builder().val(2i64)),
1817                        ])
1818                        .unwrap(),
1819                    "{a: 1, b: 2}",
1820                ),
1821                // Tags
1822                (
1823                    builder().has_tag(builder().var(ast::Var::Action), builder().val("tag")),
1824                    "action.hasTag(\"tag\")",
1825                ),
1826                (
1827                    builder().get_tag(builder().var(ast::Var::Action), builder().val("tag")),
1828                    "action.getTag(\"tag\")",
1829                ),
1830                // Like
1831                (
1832                    builder().like(
1833                        builder().val("hello"),
1834                        ast::Pattern::from(vec![
1835                            ast::PatternElem::Char('h'),
1836                            ast::PatternElem::Wildcard,
1837                        ]),
1838                    ),
1839                    "\"hello\" like \"h*\"",
1840                ),
1841                // Function calls
1842                (
1843                    builder()
1844                        .call_extension_fn(
1845                            Name::unqualified("decimal").unwrap().into(),
1846                            vec![builder().val("1.23")],
1847                        )
1848                        .unwrap(),
1849                    "decimal(\"1.23\")",
1850                ),
1851            ];
1852
1853            for (expr, expected) in cases {
1854                assert_eq!(expr.to_string(), expected, "Failed for: {}", expected);
1855            }
1856
1857            let fail_func = builder().call_extension_fn(
1858                Name::unqualified("notAFunc").unwrap().into(),
1859                vec![builder().val("12.3")],
1860            );
1861            assert!(fail_func.is_err());
1862        }
1863
1864        #[test]
1865        fn test_complex_expressions() {
1866            // Nested binary ops
1867            let nested = builder().is_eq(
1868                builder().add(builder().val(1i64), builder().val(2i64)),
1869                builder().val(3i64),
1870            );
1871            assert_eq!(nested.to_string(), "(1 + 2) == 3");
1872
1873            // Complex if-then-else
1874            let complex = builder().ite(
1875                builder().greater(
1876                    builder().get_attr(builder().var(ast::Var::Principal), SmolStr::from("age")),
1877                    builder().val(18i64),
1878                ),
1879                builder().get_attr(builder().var(ast::Var::Principal), SmolStr::from("name")),
1880                builder().val("unknown"),
1881            );
1882            assert_eq!(
1883                complex.to_string(),
1884                "if ((principal.age) > 18) then (principal.name) else \"unknown\""
1885            );
1886
1887            // isEmpty
1888            let is_empty = builder().is_empty(builder().set([]));
1889            assert_eq!(is_empty.to_string(), "[].isEmpty()");
1890        }
1891
1892        #[test]
1893        fn test_unary_op_display_no_impossible_operator() {
1894            // Test that all UnaryOp variants display without showing "<impossible operator>"
1895            let ops = [
1896                UnaryOp::Not,
1897                UnaryOp::Neg,
1898                UnaryOp::IsEmpty,
1899                UnaryOp::Datetime,
1900                UnaryOp::Decimal,
1901                UnaryOp::Duration,
1902                UnaryOp::Ip,
1903                UnaryOp::IsIPv4,
1904                UnaryOp::IsIPV6,
1905                UnaryOp::IsLoopback,
1906                UnaryOp::IsMulticast,
1907                UnaryOp::ToDate,
1908                UnaryOp::ToTime,
1909                UnaryOp::ToMilliseconds,
1910                UnaryOp::ToSeconds,
1911                UnaryOp::ToMinutes,
1912                UnaryOp::ToHours,
1913                UnaryOp::ToDays,
1914            ];
1915
1916            for op in ops {
1917                let display = op.to_string();
1918                assert_ne!(
1919                    display, "<impossible operator>",
1920                    "UnaryOp::{:?} should not display as impossible operator",
1921                    op
1922                );
1923            }
1924        }
1925
1926        #[test]
1927        fn test_binary_op_display_no_impossible_operator() {
1928            // Test that all BinaryOp variants display without showing "<impossible operator>"
1929            let ops = [
1930                BinaryOp::Eq,
1931                BinaryOp::NotEq,
1932                BinaryOp::Less,
1933                BinaryOp::LessEq,
1934                BinaryOp::Greater,
1935                BinaryOp::GreaterEq,
1936                BinaryOp::And,
1937                BinaryOp::Or,
1938                BinaryOp::Add,
1939                BinaryOp::Sub,
1940                BinaryOp::Mul,
1941                BinaryOp::In,
1942                BinaryOp::Contains,
1943                BinaryOp::ContainsAll,
1944                BinaryOp::ContainsAny,
1945                BinaryOp::GetTag,
1946                BinaryOp::HasTag,
1947                BinaryOp::IsInRange,
1948                BinaryOp::Offset,
1949                BinaryOp::DurationSince,
1950            ];
1951
1952            for op in ops {
1953                let display = op.to_string();
1954                assert_ne!(
1955                    display, "<impossible operator>",
1956                    "BinaryOp::{:?} should not display as impossible operator",
1957                    op
1958                );
1959            }
1960        }
1961    }
1962}