Skip to main content

php_ast/
ast.rs

1use std::borrow::Cow;
2
3use serde::Serialize;
4
5use crate::Span;
6
7fn is_false(b: &bool) -> bool {
8    !*b
9}
10
11/// Arena-allocated Vec. Thin newtype over bumpalo::collections::Vec that implements Serialize and Debug.
12pub struct ArenaVec<'arena, T>(bumpalo::collections::Vec<'arena, T>);
13
14impl<'arena, T> ArenaVec<'arena, T> {
15    #[inline]
16    pub fn new_in(arena: &'arena bumpalo::Bump) -> Self {
17        Self(bumpalo::collections::Vec::new_in(arena))
18    }
19    #[inline]
20    pub fn with_capacity_in(cap: usize, arena: &'arena bumpalo::Bump) -> Self {
21        Self(bumpalo::collections::Vec::with_capacity_in(cap, arena))
22    }
23    #[inline]
24    pub fn push(&mut self, val: T) {
25        self.0.push(val)
26    }
27    /// Kept as an explicit method so `"ArenaVec::is_empty"` works as a serde
28    /// `skip_serializing_if` path (deref-inherited methods don't resolve via UFCS).
29    #[inline]
30    pub fn is_empty(&self) -> bool {
31        self.0.is_empty()
32    }
33}
34
35impl<'arena, T> IntoIterator for ArenaVec<'arena, T> {
36    type Item = T;
37    type IntoIter = bumpalo::collections::vec::IntoIter<'arena, T>;
38    #[inline]
39    fn into_iter(self) -> Self::IntoIter {
40        self.0.into_iter()
41    }
42}
43
44impl<'arena, T> std::ops::Deref for ArenaVec<'arena, T> {
45    type Target = [T];
46    #[inline]
47    fn deref(&self) -> &[T] {
48        &self.0
49    }
50}
51
52impl<'arena, T> std::ops::DerefMut for ArenaVec<'arena, T> {
53    #[inline]
54    fn deref_mut(&mut self) -> &mut [T] {
55        &mut self.0
56    }
57}
58
59impl<'arena, T: serde::Serialize> serde::Serialize for ArenaVec<'arena, T> {
60    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
61        self.0.as_slice().serialize(s)
62    }
63}
64
65impl<'arena, T: std::fmt::Debug> std::fmt::Debug for ArenaVec<'arena, T> {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        self.0.as_slice().fmt(f)
68    }
69}
70
71/// A comment found in the source file.
72#[derive(Debug, Serialize)]
73pub struct Comment<'src> {
74    pub kind: CommentKind,
75    /// Raw text of the comment including its delimiters (e.g. `// foo`, `/* bar */`, `/** baz */`).
76    pub text: &'src str,
77    pub span: Span,
78}
79
80/// Distinguishes the four syntactic forms of PHP comment.
81#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq)]
82pub enum CommentKind {
83    /// `// …` — single-line slash comment
84    Line,
85    /// `# …` — single-line hash comment
86    Hash,
87    /// `/* … */` — block comment
88    Block,
89    /// `/** … */` — doc-block comment (first non-whitespace char after `/*` is `*`)
90    Doc,
91}
92
93// =============================================================================
94/// The root AST node representing a complete PHP file.
95#[derive(Debug, Serialize)]
96pub struct Program<'arena, 'src> {
97    pub stmts: ArenaVec<'arena, Stmt<'arena, 'src>>,
98    pub span: Span,
99}
100
101// =============================================================================
102// Names and Types
103// =============================================================================
104
105/// A PHP name (identifier, qualified name, fully-qualified name, or relative name).
106///
107/// The `Simple` variant is the fast path for the common case (~95%) of single
108/// unqualified identifiers like `strlen`, `Foo`, `MyClass`. It avoids allocating
109/// an `ArenaVec` entirely.
110///
111/// The `Complex` variant handles qualified (`Foo\Bar`), fully-qualified (`\Foo\Bar`),
112/// and relative (`namespace\Foo`) names.
113pub enum Name<'arena, 'src> {
114    /// Single unqualified identifier — no `ArenaVec` allocation.
115    /// `&'src str` instead of `Cow` since this is always a borrowed slice of the source.
116    Simple { value: &'src str, span: Span },
117    /// Multi-part or prefixed name (`Foo\Bar`, `\Foo`, `namespace\Foo`).
118    Complex {
119        parts: ArenaVec<'arena, &'src str>,
120        kind: NameKind,
121        span: Span,
122    },
123}
124
125impl<'arena, 'src> Name<'arena, 'src> {
126    #[inline]
127    pub fn span(&self) -> Span {
128        match self {
129            Self::Simple { span, .. } | Self::Complex { span, .. } => *span,
130        }
131    }
132
133    #[inline]
134    pub fn kind(&self) -> NameKind {
135        match self {
136            Self::Simple { .. } => NameKind::Unqualified,
137            Self::Complex { kind, .. } => *kind,
138        }
139    }
140
141    /// Returns the name as a borrowed slice of the source string.
142    ///
143    /// Unlike [`to_string_repr`], this never allocates: it uses the stored
144    /// span to slice directly into `src`.  The slice includes any leading `\`
145    /// for fully-qualified names, exactly as it appears in the source.
146    ///
147    /// Use this when you need a zero-copy `&'src str` and already have the
148    /// source buffer available (e.g. inside [`crate::visitor::ScopeWalker`]).
149    #[inline]
150    pub fn src_repr(&self, src: &'src str) -> &'src str {
151        match self {
152            Self::Simple { value, .. } => value,
153            Self::Complex { span, .. } => &src[span.start as usize..span.end as usize],
154        }
155    }
156
157    /// Joins all parts with `\` and prepends `\` if fully qualified.
158    /// Returns `Cow::Borrowed` for simple names (zero allocation).
159    #[inline]
160    pub fn to_string_repr(&self) -> Cow<'src, str> {
161        match self {
162            Self::Simple { value, .. } => Cow::Borrowed(value),
163            Self::Complex { parts, kind, .. } => {
164                let joined = parts.join("\\");
165                if *kind == NameKind::FullyQualified {
166                    Cow::Owned(format!("\\{}", joined))
167                } else {
168                    Cow::Owned(joined)
169                }
170            }
171        }
172    }
173
174    /// Joins all parts with `\` without any leading backslash.
175    /// Returns `Cow::Borrowed` for simple names (zero allocation).
176    #[inline]
177    pub fn join_parts(&self) -> Cow<'src, str> {
178        match self {
179            Self::Simple { value, .. } => Cow::Borrowed(value),
180            Self::Complex { parts, .. } => Cow::Owned(parts.join("\\")),
181        }
182    }
183
184    /// Returns the parts as a slice.
185    /// For `Simple`, returns a single-element slice of the value.
186    #[inline]
187    pub fn parts_slice(&self) -> &[&'src str] {
188        match self {
189            Self::Simple { value, .. } => std::slice::from_ref(value),
190            Self::Complex { parts, .. } => parts,
191        }
192    }
193}
194
195impl<'arena, 'src> std::fmt::Debug for Name<'arena, 'src> {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        match self {
198            Self::Simple { value, span } => f
199                .debug_struct("Name")
200                .field("parts", &std::slice::from_ref(value))
201                .field("kind", &NameKind::Unqualified)
202                .field("span", span)
203                .finish(),
204            Self::Complex { parts, kind, span } => f
205                .debug_struct("Name")
206                .field("parts", parts)
207                .field("kind", kind)
208                .field("span", span)
209                .finish(),
210        }
211    }
212}
213
214impl<'arena, 'src> serde::Serialize for Name<'arena, 'src> {
215    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
216        use serde::ser::SerializeStruct;
217        let mut st = s.serialize_struct("Name", 3)?;
218        match self {
219            Self::Simple { value, span } => {
220                st.serialize_field("parts", std::slice::from_ref(value))?;
221                st.serialize_field("kind", &NameKind::Unqualified)?;
222                st.serialize_field("span", span)?;
223            }
224            Self::Complex { parts, kind, span } => {
225                st.serialize_field("parts", parts)?;
226                st.serialize_field("kind", kind)?;
227                st.serialize_field("span", span)?;
228            }
229        }
230        st.end()
231    }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
235pub enum NameKind {
236    /// A bare identifier with no namespace separator: `Foo`, `strlen`.
237    Unqualified,
238    /// A name with at least one internal `\` but no leading backslash: `Foo\Bar`.
239    Qualified,
240    /// A name with a leading `\`: `\Foo\Bar`.
241    FullyQualified,
242    /// A name starting with the `namespace` keyword: `namespace\Foo`.
243    Relative,
244}
245
246/// PHP built-in type keyword — zero-cost alternative to `Name::Simple` for the
247/// 20 reserved type names. One byte instead of a `Cow<str>` + `Span` in the AST.
248#[repr(u8)]
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
250pub enum BuiltinType {
251    /// `int` — integer scalar type.
252    Int,
253    /// `integer` — alias for `int`, accepted in type casts.
254    Integer,
255    /// `float` — floating-point scalar type.
256    Float,
257    /// `double` — alias for `float`, accepted in type casts.
258    Double,
259    /// `string` — string scalar type.
260    String,
261    /// `bool` — boolean scalar type.
262    Bool,
263    /// `boolean` — alias for `bool`, accepted in type casts.
264    Boolean,
265    /// `void` — return-only type indicating no value is returned.
266    Void,
267    /// `never` — return-only type for functions that never return normally (PHP 8.1+).
268    Never,
269    /// `mixed` — top type; accepts any value.
270    Mixed,
271    /// `object` — any object instance.
272    Object,
273    /// `iterable` — `array` or `Traversable` (deprecated in PHP 8.2; use `array|Traversable`).
274    Iterable,
275    /// `callable` — any callable value.
276    Callable,
277    /// `array` — any PHP array.
278    Array,
279    /// `self` — refers to the class in which the type hint appears.
280    Self_,
281    /// `parent` — refers to the parent class of the class in which the type hint appears.
282    Parent_,
283    /// `static` — late-static-bound type; the class on which the method was called.
284    Static,
285    /// `null` — the null type; only valid in union types.
286    Null,
287    /// `true` — the literal boolean `true` (PHP 8.2+).
288    True,
289    /// `false` — the literal boolean `false`.
290    False,
291}
292
293impl BuiltinType {
294    /// Returns the canonical lowercase spelling used in PHP and in serialized output.
295    #[inline]
296    pub fn as_str(self) -> &'static str {
297        match self {
298            Self::Int => "int",
299            Self::Integer => "integer",
300            Self::Float => "float",
301            Self::Double => "double",
302            Self::String => "string",
303            Self::Bool => "bool",
304            Self::Boolean => "boolean",
305            Self::Void => "void",
306            Self::Never => "never",
307            Self::Mixed => "mixed",
308            Self::Object => "object",
309            Self::Iterable => "iterable",
310            Self::Callable => "callable",
311            Self::Array => "array",
312            Self::Self_ => "self",
313            Self::Parent_ => "parent",
314            Self::Static => "static",
315            Self::Null => "null",
316            Self::True => "true",
317            Self::False => "false",
318        }
319    }
320}
321
322#[derive(Debug, Serialize)]
323pub struct TypeHint<'arena, 'src> {
324    pub kind: TypeHintKind<'arena, 'src>,
325    pub span: Span,
326}
327
328/// A PHP type hint.
329///
330/// `Keyword` is the fast path for the 20 built-in type names (`int`, `string`,
331/// `bool`, `self`, `array`, etc.). It stores only a 1-byte discriminant and a
332/// `Span`, avoiding the `Cow<str>` that `Named(Name::Simple)` would require.
333///
334/// Serialises identically to `Named` so all existing snapshots remain unchanged.
335#[derive(Debug)]
336pub enum TypeHintKind<'arena, 'src> {
337    /// A user-defined or qualified class name: `Foo`, `\Ns\Bar`.
338    Named(Name<'arena, 'src>),
339    /// Built-in type keyword (`int`, `string`, `bool`, `self`, …) — serialises as `Named` for snapshot compatibility.
340    Keyword(BuiltinType, Span),
341    /// Nullable type: `?T` — equivalent to `T|null`.
342    Nullable(&'arena TypeHint<'arena, 'src>),
343    /// Union type: `A|B|C` (PHP 8.0+).
344    Union(ArenaVec<'arena, TypeHint<'arena, 'src>>),
345    /// Intersection type: `A&B` (PHP 8.1+).
346    Intersection(ArenaVec<'arena, TypeHint<'arena, 'src>>),
347}
348
349impl<'arena, 'src> serde::Serialize for TypeHintKind<'arena, 'src> {
350    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
351        match self {
352            // Standard variants — match what #[derive(Serialize)] would produce.
353            Self::Named(name) => s.serialize_newtype_variant("TypeHintKind", 0, "Named", name),
354            Self::Nullable(inner) => {
355                s.serialize_newtype_variant("TypeHintKind", 2, "Nullable", inner)
356            }
357            Self::Union(types) => s.serialize_newtype_variant("TypeHintKind", 3, "Union", types),
358            Self::Intersection(types) => {
359                s.serialize_newtype_variant("TypeHintKind", 4, "Intersection", types)
360            }
361            // Keyword — serialise as if it were Named(Name::Simple { value: kw.as_str(), span }).
362            // This preserves all existing snapshot output.
363            Self::Keyword(builtin, span) => {
364                struct BuiltinNameRepr<'a>(&'a BuiltinType, &'a Span);
365                impl serde::Serialize for BuiltinNameRepr<'_> {
366                    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
367                        use serde::ser::SerializeStruct;
368                        let mut st = s.serialize_struct("Name", 3)?;
369                        st.serialize_field("parts", &[self.0.as_str()])?;
370                        st.serialize_field("kind", &NameKind::Unqualified)?;
371                        st.serialize_field("span", self.1)?;
372                        st.end()
373                    }
374                }
375                s.serialize_newtype_variant(
376                    "TypeHintKind",
377                    0,
378                    "Named",
379                    &BuiltinNameRepr(builtin, span),
380                )
381            }
382        }
383    }
384}
385
386// =============================================================================
387// Arguments
388// =============================================================================
389
390#[derive(Debug, Serialize)]
391pub struct Arg<'arena, 'src> {
392    pub name: Option<Name<'arena, 'src>>,
393    pub value: Expr<'arena, 'src>,
394    pub unpack: bool,
395    pub by_ref: bool,
396    pub span: Span,
397}
398
399// =============================================================================
400// Attributes
401// =============================================================================
402
403#[derive(Debug, Serialize)]
404pub struct Attribute<'arena, 'src> {
405    pub name: Name<'arena, 'src>,
406    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
407    pub span: Span,
408}
409
410// =============================================================================
411// Statements
412// =============================================================================
413
414#[derive(Debug, Serialize)]
415pub struct Stmt<'arena, 'src> {
416    pub kind: StmtKind<'arena, 'src>,
417    pub span: Span,
418}
419
420#[derive(Debug, Serialize)]
421pub enum StmtKind<'arena, 'src> {
422    /// Expression statement (e.g. `foo();`)
423    Expression(&'arena Expr<'arena, 'src>),
424
425    /// Echo statement: `echo expr1, expr2;`
426    Echo(ArenaVec<'arena, Expr<'arena, 'src>>),
427
428    /// Return statement: `return expr;`
429    Return(Option<&'arena Expr<'arena, 'src>>),
430
431    /// Block statement: `{ stmts }`
432    Block(ArenaVec<'arena, Stmt<'arena, 'src>>),
433
434    /// If statement
435    If(&'arena IfStmt<'arena, 'src>),
436
437    /// While loop
438    While(&'arena WhileStmt<'arena, 'src>),
439
440    /// For loop
441    For(&'arena ForStmt<'arena, 'src>),
442
443    /// Foreach loop
444    Foreach(&'arena ForeachStmt<'arena, 'src>),
445
446    /// Do-while loop
447    DoWhile(&'arena DoWhileStmt<'arena, 'src>),
448
449    /// Function declaration
450    Function(&'arena FunctionDecl<'arena, 'src>),
451
452    /// Break statement
453    Break(Option<&'arena Expr<'arena, 'src>>),
454
455    /// Continue statement
456    Continue(Option<&'arena Expr<'arena, 'src>>),
457
458    /// Switch statement
459    Switch(&'arena SwitchStmt<'arena, 'src>),
460
461    /// Goto statement
462    Goto(&'src str),
463
464    /// Label statement
465    Label(&'arena str),
466
467    /// Declare statement
468    Declare(&'arena DeclareStmt<'arena, 'src>),
469
470    /// Unset statement
471    Unset(ArenaVec<'arena, Expr<'arena, 'src>>),
472
473    /// Throw statement (also can be expression in PHP 8)
474    Throw(&'arena Expr<'arena, 'src>),
475
476    /// Try/catch/finally
477    TryCatch(&'arena TryCatchStmt<'arena, 'src>),
478
479    /// Global declaration
480    Global(ArenaVec<'arena, Expr<'arena, 'src>>),
481
482    /// Class declaration
483    Class(&'arena ClassDecl<'arena, 'src>),
484
485    /// Interface declaration
486    Interface(&'arena InterfaceDecl<'arena, 'src>),
487
488    /// Trait declaration
489    Trait(&'arena TraitDecl<'arena, 'src>),
490
491    /// Enum declaration
492    Enum(&'arena EnumDecl<'arena, 'src>),
493
494    /// Namespace declaration
495    Namespace(&'arena NamespaceDecl<'arena, 'src>),
496
497    /// Use declaration
498    Use(&'arena UseDecl<'arena, 'src>),
499
500    /// Top-level constant: `const FOO = expr;`
501    Const(ArenaVec<'arena, ConstItem<'arena, 'src>>),
502
503    /// Static variable declaration: `static $x = 1;`
504    StaticVar(ArenaVec<'arena, StaticVar<'arena, 'src>>),
505
506    /// __halt_compiler(); with remaining data
507    HaltCompiler(&'src str),
508
509    /// Nop (empty statement `;`)
510    Nop,
511
512    /// Inline HTML
513    InlineHtml(&'src str),
514
515    /// Error placeholder — parser always produces a tree
516    Error,
517}
518
519#[derive(Debug, Serialize)]
520pub struct IfStmt<'arena, 'src> {
521    pub condition: Expr<'arena, 'src>,
522    pub then_branch: &'arena Stmt<'arena, 'src>,
523    pub elseif_branches: ArenaVec<'arena, ElseIfBranch<'arena, 'src>>,
524    pub else_branch: Option<&'arena Stmt<'arena, 'src>>,
525}
526
527#[derive(Debug, Serialize)]
528pub struct ElseIfBranch<'arena, 'src> {
529    pub condition: Expr<'arena, 'src>,
530    pub body: Stmt<'arena, 'src>,
531    pub span: Span,
532}
533
534#[derive(Debug, Serialize)]
535pub struct WhileStmt<'arena, 'src> {
536    pub condition: Expr<'arena, 'src>,
537    pub body: &'arena Stmt<'arena, 'src>,
538}
539
540#[derive(Debug, Serialize)]
541pub struct ForStmt<'arena, 'src> {
542    pub init: ArenaVec<'arena, Expr<'arena, 'src>>,
543    pub condition: ArenaVec<'arena, Expr<'arena, 'src>>,
544    pub update: ArenaVec<'arena, Expr<'arena, 'src>>,
545    pub body: &'arena Stmt<'arena, 'src>,
546}
547
548#[derive(Debug, Serialize)]
549pub struct ForeachStmt<'arena, 'src> {
550    pub expr: Expr<'arena, 'src>,
551    pub key: Option<Expr<'arena, 'src>>,
552    pub value: Expr<'arena, 'src>,
553    pub body: &'arena Stmt<'arena, 'src>,
554}
555
556#[derive(Debug, Serialize)]
557pub struct DoWhileStmt<'arena, 'src> {
558    pub body: &'arena Stmt<'arena, 'src>,
559    pub condition: Expr<'arena, 'src>,
560}
561
562#[derive(Debug, Serialize)]
563pub struct FunctionDecl<'arena, 'src> {
564    pub name: &'src str,
565    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
566    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
567    pub return_type: Option<TypeHint<'arena, 'src>>,
568    pub by_ref: bool,
569    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub doc_comment: Option<Comment<'src>>,
572}
573
574#[derive(Debug, Serialize)]
575pub struct Param<'arena, 'src> {
576    pub name: &'src str,
577    pub type_hint: Option<TypeHint<'arena, 'src>>,
578    pub default: Option<Expr<'arena, 'src>>,
579    pub by_ref: bool,
580    pub variadic: bool,
581    pub is_readonly: bool,
582    pub is_final: bool,
583    pub visibility: Option<Visibility>,
584    pub set_visibility: Option<Visibility>,
585    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
586    #[serde(skip_serializing_if = "ArenaVec::is_empty")]
587    pub hooks: ArenaVec<'arena, PropertyHook<'arena, 'src>>,
588    pub span: Span,
589}
590
591#[derive(Debug, Serialize)]
592pub struct SwitchStmt<'arena, 'src> {
593    pub expr: Expr<'arena, 'src>,
594    pub cases: ArenaVec<'arena, SwitchCase<'arena, 'src>>,
595}
596
597#[derive(Debug, Serialize)]
598pub struct SwitchCase<'arena, 'src> {
599    pub value: Option<Expr<'arena, 'src>>,
600    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
601    pub span: Span,
602}
603
604#[derive(Debug, Serialize)]
605pub struct TryCatchStmt<'arena, 'src> {
606    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
607    pub catches: ArenaVec<'arena, CatchClause<'arena, 'src>>,
608    pub finally: Option<ArenaVec<'arena, Stmt<'arena, 'src>>>,
609}
610
611#[derive(Debug, Serialize)]
612pub struct CatchClause<'arena, 'src> {
613    pub types: ArenaVec<'arena, Name<'arena, 'src>>,
614    pub var: Option<&'src str>,
615    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
616    pub span: Span,
617}
618
619// =============================================================================
620// OOP Declarations
621// =============================================================================
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
624pub enum Visibility {
625    /// `public` — accessible from anywhere.
626    Public,
627    /// `protected` — accessible within the class and its subclasses.
628    Protected,
629    /// `private` — accessible only within the declaring class.
630    Private,
631}
632
633#[derive(Debug, Serialize)]
634pub struct ClassDecl<'arena, 'src> {
635    pub name: Option<&'src str>,
636    pub modifiers: ClassModifiers,
637    pub extends: Option<Name<'arena, 'src>>,
638    pub implements: ArenaVec<'arena, Name<'arena, 'src>>,
639    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
640    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub doc_comment: Option<Comment<'src>>,
643}
644
645#[derive(Debug, Clone, Serialize, Default)]
646pub struct ClassModifiers {
647    pub is_abstract: bool,
648    pub is_final: bool,
649    pub is_readonly: bool,
650}
651
652#[derive(Debug, Serialize)]
653pub struct ClassMember<'arena, 'src> {
654    pub kind: ClassMemberKind<'arena, 'src>,
655    pub span: Span,
656}
657
658#[derive(Debug, Serialize)]
659pub enum ClassMemberKind<'arena, 'src> {
660    Property(PropertyDecl<'arena, 'src>),
661    Method(MethodDecl<'arena, 'src>),
662    ClassConst(ClassConstDecl<'arena, 'src>),
663    TraitUse(TraitUseDecl<'arena, 'src>),
664}
665
666#[derive(Debug, Serialize)]
667pub struct PropertyDecl<'arena, 'src> {
668    pub name: &'src str,
669    pub visibility: Option<Visibility>,
670    pub set_visibility: Option<Visibility>,
671    pub is_static: bool,
672    pub is_readonly: bool,
673    pub type_hint: Option<TypeHint<'arena, 'src>>,
674    pub default: Option<Expr<'arena, 'src>>,
675    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
676    #[serde(skip_serializing_if = "ArenaVec::is_empty")]
677    pub hooks: ArenaVec<'arena, PropertyHook<'arena, 'src>>,
678    #[serde(skip_serializing_if = "Option::is_none")]
679    pub doc_comment: Option<Comment<'src>>,
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
683pub enum PropertyHookKind {
684    /// `get` hook — called when the property is read.
685    Get,
686    /// `set` hook — called when the property is written; receives the incoming value as `$value`.
687    Set,
688}
689
690#[derive(Debug, Serialize)]
691pub enum PropertyHookBody<'arena, 'src> {
692    /// `{ stmts }` — a full statement block.
693    Block(ArenaVec<'arena, Stmt<'arena, 'src>>),
694    /// `=> expr` — short-form expression body.
695    Expression(Expr<'arena, 'src>),
696    /// No body — the hook is declared abstract (on an abstract class or interface).
697    Abstract,
698}
699
700#[derive(Debug, Serialize)]
701pub struct PropertyHook<'arena, 'src> {
702    pub kind: PropertyHookKind,
703    pub body: PropertyHookBody<'arena, 'src>,
704    pub is_final: bool,
705    pub by_ref: bool,
706    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
707    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
708    pub span: Span,
709}
710
711#[derive(Debug, Serialize)]
712pub struct MethodDecl<'arena, 'src> {
713    pub name: &'src str,
714    pub visibility: Option<Visibility>,
715    pub is_static: bool,
716    pub is_abstract: bool,
717    pub is_final: bool,
718    pub by_ref: bool,
719    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
720    pub return_type: Option<TypeHint<'arena, 'src>>,
721    pub body: Option<ArenaVec<'arena, Stmt<'arena, 'src>>>,
722    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
723    #[serde(skip_serializing_if = "Option::is_none")]
724    pub doc_comment: Option<Comment<'src>>,
725}
726
727#[derive(Debug, Serialize)]
728pub struct ClassConstDecl<'arena, 'src> {
729    pub name: &'src str,
730    pub visibility: Option<Visibility>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub type_hint: Option<&'arena TypeHint<'arena, 'src>>,
733    pub value: Expr<'arena, 'src>,
734    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub doc_comment: Option<Comment<'src>>,
737}
738
739#[derive(Debug, Serialize)]
740pub struct TraitUseDecl<'arena, 'src> {
741    pub traits: ArenaVec<'arena, Name<'arena, 'src>>,
742    pub adaptations: ArenaVec<'arena, TraitAdaptation<'arena, 'src>>,
743}
744
745#[derive(Debug, Serialize)]
746pub struct TraitAdaptation<'arena, 'src> {
747    pub kind: TraitAdaptationKind<'arena, 'src>,
748    pub span: Span,
749}
750
751#[derive(Debug, Serialize)]
752pub enum TraitAdaptationKind<'arena, 'src> {
753    /// `A::foo insteadof B, C;`
754    Precedence {
755        trait_name: Name<'arena, 'src>,
756        method: Name<'arena, 'src>,
757        insteadof: ArenaVec<'arena, Name<'arena, 'src>>,
758    },
759    /// `foo as bar;` or `A::foo as protected bar;` or `foo as protected;`
760    Alias {
761        trait_name: Option<Name<'arena, 'src>>,
762        method: Name<'arena, 'src>,
763        new_modifier: Option<Visibility>,
764        new_name: Option<Name<'arena, 'src>>,
765    },
766}
767
768#[derive(Debug, Serialize)]
769pub struct InterfaceDecl<'arena, 'src> {
770    pub name: &'src str,
771    pub extends: ArenaVec<'arena, Name<'arena, 'src>>,
772    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
773    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
774    #[serde(skip_serializing_if = "Option::is_none")]
775    pub doc_comment: Option<Comment<'src>>,
776}
777
778#[derive(Debug, Serialize)]
779pub struct TraitDecl<'arena, 'src> {
780    pub name: &'src str,
781    pub members: ArenaVec<'arena, ClassMember<'arena, 'src>>,
782    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub doc_comment: Option<Comment<'src>>,
785}
786
787#[derive(Debug, Serialize)]
788pub struct EnumDecl<'arena, 'src> {
789    pub name: &'src str,
790    pub scalar_type: Option<Name<'arena, 'src>>,
791    pub implements: ArenaVec<'arena, Name<'arena, 'src>>,
792    pub members: ArenaVec<'arena, EnumMember<'arena, 'src>>,
793    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub doc_comment: Option<Comment<'src>>,
796}
797
798#[derive(Debug, Serialize)]
799pub struct EnumMember<'arena, 'src> {
800    pub kind: EnumMemberKind<'arena, 'src>,
801    pub span: Span,
802}
803
804#[derive(Debug, Serialize)]
805pub enum EnumMemberKind<'arena, 'src> {
806    /// An enum case: `case Foo;` or `case Foo = 'foo';` (backed enum).
807    Case(EnumCase<'arena, 'src>),
808    /// A method defined inside the enum body.
809    Method(MethodDecl<'arena, 'src>),
810    /// A constant defined inside the enum body: `const X = 1;`.
811    ClassConst(ClassConstDecl<'arena, 'src>),
812    /// A trait use inside the enum body: `use SomeTrait;`.
813    TraitUse(TraitUseDecl<'arena, 'src>),
814}
815
816#[derive(Debug, Serialize)]
817pub struct EnumCase<'arena, 'src> {
818    pub name: &'src str,
819    pub value: Option<Expr<'arena, 'src>>,
820    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub doc_comment: Option<Comment<'src>>,
823}
824
825// =============================================================================
826// Namespace & Use
827// =============================================================================
828
829#[derive(Debug, Serialize)]
830pub struct NamespaceDecl<'arena, 'src> {
831    pub name: Option<Name<'arena, 'src>>,
832    pub body: NamespaceBody<'arena, 'src>,
833}
834
835#[derive(Debug, Serialize)]
836pub enum NamespaceBody<'arena, 'src> {
837    /// `namespace Foo { … }` — braced form; the statements are scoped to this namespace.
838    Braced(ArenaVec<'arena, Stmt<'arena, 'src>>),
839    /// `namespace Foo;` — simple form; all subsequent statements until the next `namespace` or EOF are in scope.
840    Simple,
841}
842
843#[derive(Debug, Serialize)]
844pub struct DeclareStmt<'arena, 'src> {
845    pub directives: ArenaVec<'arena, (&'src str, Expr<'arena, 'src>)>,
846    pub body: Option<&'arena Stmt<'arena, 'src>>,
847}
848
849#[derive(Debug, Serialize)]
850pub struct UseDecl<'arena, 'src> {
851    pub kind: UseKind,
852    pub uses: ArenaVec<'arena, UseItem<'arena, 'src>>,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
856pub enum UseKind {
857    /// `use Foo\Bar` — imports a class, interface, trait, or enum.
858    Normal,
859    /// `use function Foo\bar` — imports a function.
860    Function,
861    /// `use const Foo\BAR` — imports a constant.
862    Const,
863}
864
865#[derive(Debug, Serialize)]
866pub struct UseItem<'arena, 'src> {
867    pub name: Name<'arena, 'src>,
868    pub alias: Option<&'src str>,
869    #[serde(skip_serializing_if = "Option::is_none")]
870    pub kind: Option<UseKind>,
871    pub span: Span,
872}
873
874#[derive(Debug, Serialize)]
875pub struct ConstItem<'arena, 'src> {
876    pub name: &'src str,
877    pub value: Expr<'arena, 'src>,
878    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
879    pub span: Span,
880    #[serde(skip_serializing_if = "Option::is_none")]
881    pub doc_comment: Option<Comment<'src>>,
882}
883
884#[derive(Debug, Serialize)]
885pub struct StaticVar<'arena, 'src> {
886    pub name: &'src str,
887    pub default: Option<Expr<'arena, 'src>>,
888    pub span: Span,
889}
890
891// =============================================================================
892// Expressions
893// =============================================================================
894
895/// A name string that originates either from the source buffer (`&'src str`) or was
896/// constructed in the arena (`&'arena str`).
897///
898/// Using this as the payload for both `ExprKind::Variable` and `ExprKind::Identifier`
899/// gives them the same binding type, so or-patterns compile natively:
900///
901/// ```
902/// # use php_ast::ast::{ExprKind, NameStr};
903/// # fn example<'a, 'b>(kind: &ExprKind<'a, 'b>) {
904/// if let ExprKind::Variable(name) | ExprKind::Identifier(name) = kind {
905///     let _s: &str = name.as_str();
906/// }
907/// # }
908/// ```
909#[derive(Clone, Copy, PartialEq, Eq, Hash)]
910pub enum NameStr<'arena, 'src> {
911    /// Borrowed directly from the source buffer.
912    Src(&'src str),
913    /// Allocated in the bump arena (e.g. a joined qualified name).
914    Arena(&'arena str),
915}
916
917impl<'arena, 'src> NameStr<'arena, 'src> {
918    #[inline]
919    pub fn as_str(&self) -> &str {
920        match self {
921            NameStr::Src(s) => s,
922            NameStr::Arena(s) => s,
923        }
924    }
925}
926
927impl<'arena, 'src> std::ops::Deref for NameStr<'arena, 'src> {
928    type Target = str;
929    #[inline]
930    fn deref(&self) -> &str {
931        self.as_str()
932    }
933}
934
935impl<'arena, 'src> std::fmt::Debug for NameStr<'arena, 'src> {
936    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
937        self.as_str().fmt(f)
938    }
939}
940
941impl<'arena, 'src> serde::Serialize for NameStr<'arena, 'src> {
942    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
943        self.as_str().serialize(serializer)
944    }
945}
946
947#[derive(Debug, Serialize)]
948pub struct Expr<'arena, 'src> {
949    pub kind: ExprKind<'arena, 'src>,
950    pub span: Span,
951}
952
953#[derive(Debug, Serialize)]
954pub enum ExprKind<'arena, 'src> {
955    /// Integer literal
956    Int(i64),
957
958    /// Float literal
959    Float(f64),
960
961    /// String literal
962    String(&'arena str),
963
964    /// Interpolated string: `"Hello $name, you are {$age} years old"`
965    InterpolatedString(ArenaVec<'arena, StringPart<'arena, 'src>>),
966
967    /// Heredoc: `<<<EOT ... EOT`
968    Heredoc {
969        label: &'src str,
970        parts: ArenaVec<'arena, StringPart<'arena, 'src>>,
971    },
972
973    /// Nowdoc: `<<<'EOT' ... EOT`
974    Nowdoc {
975        label: &'src str,
976        value: &'arena str,
977    },
978
979    /// Shell execution: `` `command $var` ``
980    ShellExec(ArenaVec<'arena, StringPart<'arena, 'src>>),
981
982    /// Boolean literal
983    Bool(bool),
984
985    /// Null literal
986    Null,
987
988    /// Variable: `$name`
989    Variable(NameStr<'arena, 'src>),
990
991    /// Variable variable: `$$var`, `$$$var`, `${expr}`
992    VariableVariable(&'arena Expr<'arena, 'src>),
993
994    /// Identifier (bare name, e.g. function name in a call)
995    Identifier(NameStr<'arena, 'src>),
996
997    /// Assignment: `$x = expr` or `$x += expr`
998    Assign(AssignExpr<'arena, 'src>),
999
1000    /// Binary operation: `expr op expr`
1001    Binary(BinaryExpr<'arena, 'src>),
1002
1003    /// Unary prefix: `-expr`, `!expr`, `~expr`, `++$x`, `--$x`
1004    UnaryPrefix(UnaryPrefixExpr<'arena, 'src>),
1005
1006    /// Unary postfix: `$x++`, `$x--`
1007    UnaryPostfix(UnaryPostfixExpr<'arena, 'src>),
1008
1009    /// Ternary: `cond ? then : else` or short `cond ?: else`
1010    Ternary(TernaryExpr<'arena, 'src>),
1011
1012    /// Null coalescing: `expr ?? fallback`
1013    NullCoalesce(NullCoalesceExpr<'arena, 'src>),
1014
1015    /// Function call: `name(args)`
1016    FunctionCall(FunctionCallExpr<'arena, 'src>),
1017
1018    /// Array literal: `[1, 2, 3]` or `['a' => 1]`
1019    Array(ArenaVec<'arena, ArrayElement<'arena, 'src>>),
1020
1021    /// Array access: `$arr[index]`
1022    ArrayAccess(ArrayAccessExpr<'arena, 'src>),
1023
1024    /// Print expression: `print expr`
1025    Print(&'arena Expr<'arena, 'src>),
1026
1027    /// Parenthesized expression: `(expr)`
1028    Parenthesized(&'arena Expr<'arena, 'src>),
1029
1030    /// Cast expression: `(int)$x`, `(string)$x`, etc.
1031    Cast(CastKind, &'arena Expr<'arena, 'src>),
1032
1033    /// Error suppression: `@expr`
1034    ErrorSuppress(&'arena Expr<'arena, 'src>),
1035
1036    /// Isset: `isset($a, $b)`
1037    Isset(ArenaVec<'arena, Expr<'arena, 'src>>),
1038
1039    /// Empty: `empty($a)`
1040    Empty(&'arena Expr<'arena, 'src>),
1041
1042    /// Include/require: `include 'file.php'`
1043    Include(IncludeKind, &'arena Expr<'arena, 'src>),
1044
1045    /// Eval: `eval('code')`
1046    Eval(&'arena Expr<'arena, 'src>),
1047
1048    /// Exit/die: `exit`, `exit(1)`, `die('msg')`
1049    Exit(Option<&'arena Expr<'arena, 'src>>),
1050
1051    /// Magic constant: `__LINE__`, `__FILE__`, etc.
1052    MagicConst(MagicConstKind),
1053
1054    /// Clone: `clone $obj`
1055    Clone(&'arena Expr<'arena, 'src>),
1056
1057    /// Clone with property overrides: `clone($obj, ['prop' => $val])` — PHP 8.5+
1058    CloneWith(&'arena Expr<'arena, 'src>, &'arena Expr<'arena, 'src>),
1059
1060    /// New: `new Class(args)`
1061    New(NewExpr<'arena, 'src>),
1062
1063    /// Property access: `$obj->prop`
1064    PropertyAccess(PropertyAccessExpr<'arena, 'src>),
1065
1066    /// Nullsafe property access: `$obj?->prop`
1067    NullsafePropertyAccess(PropertyAccessExpr<'arena, 'src>),
1068
1069    /// Method call: `$obj->method(args)`
1070    MethodCall(&'arena MethodCallExpr<'arena, 'src>),
1071
1072    /// Nullsafe method call: `$obj?->method(args)`
1073    NullsafeMethodCall(&'arena MethodCallExpr<'arena, 'src>),
1074
1075    /// Static property access: `Class::$prop`
1076    StaticPropertyAccess(StaticAccessExpr<'arena, 'src>),
1077
1078    /// Static method call: `Class::method(args)`
1079    StaticMethodCall(&'arena StaticMethodCallExpr<'arena, 'src>),
1080
1081    /// Dynamic static method call: `Class::$method(args)`
1082    StaticDynMethodCall(&'arena StaticDynMethodCallExpr<'arena, 'src>),
1083
1084    /// Class constant access: `Class::CONST`
1085    ClassConstAccess(StaticAccessExpr<'arena, 'src>),
1086
1087    /// Dynamic class constant access: `Foo::{expr}`
1088    ClassConstAccessDynamic {
1089        class: &'arena Expr<'arena, 'src>,
1090        member: &'arena Expr<'arena, 'src>,
1091    },
1092
1093    /// Dynamic static property access: `A::$$b`, `A::${'b'}`
1094    StaticPropertyAccessDynamic {
1095        class: &'arena Expr<'arena, 'src>,
1096        member: &'arena Expr<'arena, 'src>,
1097    },
1098
1099    /// Closure: `function($x) use($y) { }`
1100    Closure(&'arena ClosureExpr<'arena, 'src>),
1101
1102    /// Arrow function: `fn($x) => expr`
1103    ArrowFunction(&'arena ArrowFunctionExpr<'arena, 'src>),
1104
1105    /// Match: `match(expr) { ... }`
1106    Match(MatchExpr<'arena, 'src>),
1107
1108    /// Throw as expression (PHP 8)
1109    ThrowExpr(&'arena Expr<'arena, 'src>),
1110
1111    /// Yield: `yield` / `yield $val` / `yield $key => $val`
1112    Yield(YieldExpr<'arena, 'src>),
1113
1114    /// Anonymous class: `new class(args) extends Foo implements Bar { ... }`
1115    AnonymousClass(&'arena ClassDecl<'arena, 'src>),
1116
1117    /// First-class callable: `strlen(...)`, `$obj->method(...)`, `Foo::bar(...)`
1118    CallableCreate(CallableCreateExpr<'arena, 'src>),
1119
1120    /// Omitted element in destructuring: `[$a, , $c]` or `list($a, , $c)`
1121    Omit,
1122
1123    /// Error placeholder
1124    Error,
1125}
1126
1127impl<'arena, 'src> Expr<'arena, 'src> {
1128    /// Returns the name string for `Variable` and `Identifier` nodes, `None` for everything else.
1129    pub fn name_str(&self) -> Option<&str> {
1130        match &self.kind {
1131            ExprKind::Variable(s) | ExprKind::Identifier(s) => Some(s.as_str()),
1132            _ => None,
1133        }
1134    }
1135}
1136
1137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1138pub enum CastKind {
1139    /// `(int)` or `(integer)` cast.
1140    Int,
1141    /// `(float)`, `(double)`, or `(real)` cast.
1142    Float,
1143    /// `(string)` cast.
1144    String,
1145    /// `(bool)` or `(boolean)` cast.
1146    Bool,
1147    /// `(array)` cast.
1148    Array,
1149    /// `(object)` cast.
1150    Object,
1151    /// `(unset)` cast — deprecated; casts to `null`.
1152    Unset,
1153    /// `(void)` cast — non-standard; treated as discarding the value.
1154    Void,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1158pub enum IncludeKind {
1159    /// `include 'file.php'` — emits a warning if the file is not found.
1160    Include,
1161    /// `include_once 'file.php'` — like `include`, but skipped if the file has already been included.
1162    IncludeOnce,
1163    /// `require 'file.php'` — fatal error if the file is not found.
1164    Require,
1165    /// `require_once 'file.php'` — like `require`, but skipped if the file has already been included.
1166    RequireOnce,
1167}
1168
1169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1170pub enum MagicConstKind {
1171    /// `__CLASS__` — name of the current class, or empty string outside a class.
1172    Class,
1173    /// `__DIR__` — directory of the current file.
1174    Dir,
1175    /// `__FILE__` — absolute path of the current file.
1176    File,
1177    /// `__FUNCTION__` — name of the current function or closure.
1178    Function,
1179    /// `__LINE__` — current line number in the source file.
1180    Line,
1181    /// `__METHOD__` — name of the current method including its class: `ClassName::methodName`.
1182    Method,
1183    /// `__NAMESPACE__` — name of the current namespace, or empty string in the global namespace.
1184    Namespace,
1185    /// `__TRAIT__` — name of the current trait, or empty string outside a trait.
1186    Trait,
1187    /// `__PROPERTY__` — name of the current property inside a property hook (PHP 8.4+).
1188    Property,
1189}
1190
1191// --- Expression sub-types ---
1192
1193#[derive(Debug, Serialize)]
1194pub struct AssignExpr<'arena, 'src> {
1195    pub target: &'arena Expr<'arena, 'src>,
1196    pub op: AssignOp,
1197    pub value: &'arena Expr<'arena, 'src>,
1198    #[serde(skip_serializing_if = "is_false")]
1199    pub by_ref: bool,
1200}
1201
1202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1203pub enum AssignOp {
1204    /// `=`
1205    Assign,
1206    /// `+=`
1207    Plus,
1208    /// `-=`
1209    Minus,
1210    /// `*=`
1211    Mul,
1212    /// `/=`
1213    Div,
1214    /// `%=`
1215    Mod,
1216    /// `**=`
1217    Pow,
1218    /// `.=`
1219    Concat,
1220    /// `&=`
1221    BitwiseAnd,
1222    /// `|=`
1223    BitwiseOr,
1224    /// `^=`
1225    BitwiseXor,
1226    /// `<<=`
1227    ShiftLeft,
1228    /// `>>=`
1229    ShiftRight,
1230    /// `??=`
1231    Coalesce,
1232}
1233
1234#[derive(Debug, Serialize)]
1235pub struct BinaryExpr<'arena, 'src> {
1236    pub left: &'arena Expr<'arena, 'src>,
1237    pub op: BinaryOp,
1238    pub right: &'arena Expr<'arena, 'src>,
1239}
1240
1241#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1242pub enum BinaryOp {
1243    /// `+`
1244    Add,
1245    /// `-`
1246    Sub,
1247    /// `*`
1248    Mul,
1249    /// `/`
1250    Div,
1251    /// `%`
1252    Mod,
1253    /// `**`
1254    Pow,
1255    /// `.` — string concatenation.
1256    Concat,
1257    /// `==` — loose equality (type-coercing).
1258    Equal,
1259    /// `!=` or `<>` — loose inequality.
1260    NotEqual,
1261    /// `===` — strict equality (type and value).
1262    Identical,
1263    /// `!==` — strict inequality.
1264    NotIdentical,
1265    /// `<`
1266    Less,
1267    /// `>`
1268    Greater,
1269    /// `<=`
1270    LessOrEqual,
1271    /// `>=`
1272    GreaterOrEqual,
1273    /// `<=>` — spaceship / three-way comparison; returns -1, 0, or 1.
1274    Spaceship,
1275    /// `&&` — short-circuit boolean AND (higher precedence than `and`).
1276    BooleanAnd,
1277    /// `||` — short-circuit boolean OR (higher precedence than `or`).
1278    BooleanOr,
1279    /// `&` — bitwise AND.
1280    BitwiseAnd,
1281    /// `|` — bitwise OR.
1282    BitwiseOr,
1283    /// `^` — bitwise XOR.
1284    BitwiseXor,
1285    /// `<<` — left bit-shift.
1286    ShiftLeft,
1287    /// `>>` — right bit-shift.
1288    ShiftRight,
1289    /// `and` — boolean AND (lower precedence than `&&`).
1290    LogicalAnd,
1291    /// `or` — boolean OR (lower precedence than `||`).
1292    LogicalOr,
1293    /// `xor` — boolean XOR.
1294    LogicalXor,
1295    /// `instanceof` — type-check operator; `$x instanceof Foo`.
1296    Instanceof,
1297    /// `|>` — pipe operator (PHP 8.5+); passes the left operand as the first argument of the right callable.
1298    Pipe,
1299}
1300
1301#[derive(Debug, Serialize)]
1302pub struct UnaryPrefixExpr<'arena, 'src> {
1303    pub op: UnaryPrefixOp,
1304    pub operand: &'arena Expr<'arena, 'src>,
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1308pub enum UnaryPrefixOp {
1309    /// `-expr` — arithmetic negation.
1310    Negate,
1311    /// `+expr` — unary plus (no-op for numbers, promotes to numeric).
1312    Plus,
1313    /// `!expr` — boolean NOT.
1314    BooleanNot,
1315    /// `~expr` — bitwise NOT.
1316    BitwiseNot,
1317    /// `++$x` — pre-increment; increments then returns the new value.
1318    PreIncrement,
1319    /// `--$x` — pre-decrement; decrements then returns the new value.
1320    PreDecrement,
1321}
1322
1323#[derive(Debug, Serialize)]
1324pub struct UnaryPostfixExpr<'arena, 'src> {
1325    pub operand: &'arena Expr<'arena, 'src>,
1326    pub op: UnaryPostfixOp,
1327}
1328
1329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1330pub enum UnaryPostfixOp {
1331    /// `$x++` — post-increment; returns the current value then increments.
1332    PostIncrement,
1333    /// `$x--` — post-decrement; returns the current value then decrements.
1334    PostDecrement,
1335}
1336
1337#[derive(Debug, Serialize)]
1338pub struct TernaryExpr<'arena, 'src> {
1339    pub condition: &'arena Expr<'arena, 'src>,
1340    /// None for short ternary `$x ?: $y`
1341    pub then_expr: Option<&'arena Expr<'arena, 'src>>,
1342    pub else_expr: &'arena Expr<'arena, 'src>,
1343}
1344
1345#[derive(Debug, Serialize)]
1346pub struct NullCoalesceExpr<'arena, 'src> {
1347    pub left: &'arena Expr<'arena, 'src>,
1348    pub right: &'arena Expr<'arena, 'src>,
1349}
1350
1351#[derive(Debug, Serialize)]
1352pub struct FunctionCallExpr<'arena, 'src> {
1353    pub name: &'arena Expr<'arena, 'src>,
1354    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1355}
1356
1357#[derive(Debug, Serialize)]
1358pub struct ArrayElement<'arena, 'src> {
1359    pub key: Option<Expr<'arena, 'src>>,
1360    pub value: Expr<'arena, 'src>,
1361    pub unpack: bool,
1362    #[serde(skip_serializing_if = "is_false")]
1363    pub by_ref: bool,
1364    pub span: Span,
1365}
1366
1367#[derive(Debug, Serialize)]
1368pub struct ArrayAccessExpr<'arena, 'src> {
1369    pub array: &'arena Expr<'arena, 'src>,
1370    pub index: Option<&'arena Expr<'arena, 'src>>,
1371}
1372
1373// --- OOP Expression sub-types ---
1374
1375#[derive(Debug, Serialize)]
1376pub struct NewExpr<'arena, 'src> {
1377    pub class: &'arena Expr<'arena, 'src>,
1378    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1379}
1380
1381#[derive(Debug, Serialize)]
1382pub struct PropertyAccessExpr<'arena, 'src> {
1383    pub object: &'arena Expr<'arena, 'src>,
1384    pub property: &'arena Expr<'arena, 'src>,
1385}
1386
1387#[derive(Debug, Serialize)]
1388pub struct MethodCallExpr<'arena, 'src> {
1389    pub object: &'arena Expr<'arena, 'src>,
1390    pub method: &'arena Expr<'arena, 'src>,
1391    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1392}
1393
1394#[derive(Debug, Serialize)]
1395pub struct StaticAccessExpr<'arena, 'src> {
1396    pub class: &'arena Expr<'arena, 'src>,
1397    pub member: &'arena Expr<'arena, 'src>,
1398}
1399
1400#[derive(Debug, Serialize)]
1401pub struct StaticMethodCallExpr<'arena, 'src> {
1402    pub class: &'arena Expr<'arena, 'src>,
1403    pub method: &'arena Expr<'arena, 'src>,
1404    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1405}
1406
1407#[derive(Debug, Serialize)]
1408pub struct StaticDynMethodCallExpr<'arena, 'src> {
1409    pub class: &'arena Expr<'arena, 'src>,
1410    pub method: &'arena Expr<'arena, 'src>,
1411    pub args: ArenaVec<'arena, Arg<'arena, 'src>>,
1412}
1413
1414#[derive(Debug, Serialize)]
1415pub struct ClosureExpr<'arena, 'src> {
1416    pub is_static: bool,
1417    pub by_ref: bool,
1418    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
1419    pub use_vars: ArenaVec<'arena, ClosureUseVar<'src>>,
1420    pub return_type: Option<TypeHint<'arena, 'src>>,
1421    pub body: ArenaVec<'arena, Stmt<'arena, 'src>>,
1422    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
1423}
1424
1425#[derive(Debug, Clone, Serialize)]
1426pub struct ClosureUseVar<'src> {
1427    pub name: &'src str,
1428    pub by_ref: bool,
1429    pub span: Span,
1430}
1431
1432#[derive(Debug, Serialize)]
1433pub struct ArrowFunctionExpr<'arena, 'src> {
1434    pub is_static: bool,
1435    pub by_ref: bool,
1436    pub params: ArenaVec<'arena, Param<'arena, 'src>>,
1437    pub return_type: Option<TypeHint<'arena, 'src>>,
1438    pub body: &'arena Expr<'arena, 'src>,
1439    pub attributes: ArenaVec<'arena, Attribute<'arena, 'src>>,
1440}
1441
1442#[derive(Debug, Serialize)]
1443pub struct MatchExpr<'arena, 'src> {
1444    pub subject: &'arena Expr<'arena, 'src>,
1445    pub arms: ArenaVec<'arena, MatchArm<'arena, 'src>>,
1446}
1447
1448#[derive(Debug, Serialize)]
1449pub struct MatchArm<'arena, 'src> {
1450    /// None for `default`
1451    pub conditions: Option<ArenaVec<'arena, Expr<'arena, 'src>>>,
1452    pub body: Expr<'arena, 'src>,
1453    pub span: Span,
1454}
1455
1456#[derive(Debug, Serialize)]
1457pub struct YieldExpr<'arena, 'src> {
1458    pub key: Option<&'arena Expr<'arena, 'src>>,
1459    pub value: Option<&'arena Expr<'arena, 'src>>,
1460    /// `true` for `yield from expr` (generator delegation), `false` for plain `yield`
1461    pub is_from: bool,
1462}
1463
1464// --- First-class callable ---
1465
1466#[derive(Debug, Serialize)]
1467pub struct CallableCreateExpr<'arena, 'src> {
1468    pub kind: CallableCreateKind<'arena, 'src>,
1469}
1470
1471#[derive(Debug, Serialize)]
1472pub enum CallableCreateKind<'arena, 'src> {
1473    /// `foo(...)`, `$var(...)`, `\Ns\func(...)`
1474    Function(&'arena Expr<'arena, 'src>),
1475    /// `$obj->method(...)`
1476    Method {
1477        object: &'arena Expr<'arena, 'src>,
1478        method: &'arena Expr<'arena, 'src>,
1479    },
1480    /// `$obj?->method(...)`
1481    NullsafeMethod {
1482        object: &'arena Expr<'arena, 'src>,
1483        method: &'arena Expr<'arena, 'src>,
1484    },
1485    /// `Foo::bar(...)`
1486    StaticMethod {
1487        class: &'arena Expr<'arena, 'src>,
1488        method: &'arena Expr<'arena, 'src>,
1489    },
1490}
1491
1492// --- String interpolation ---
1493
1494#[derive(Debug, Serialize)]
1495pub enum StringPart<'arena, 'src> {
1496    /// A plain text segment of an interpolated string or heredoc.
1497    Literal(&'arena str),
1498    /// An embedded expression: `$var`, `{$expr}`, or `${var}`.
1499    Expr(Expr<'arena, 'src>),
1500}