Skip to main content

ext_php_rs/types/
zval.rs

1//! The base value in PHP. A Zval can contain any PHP type, and the type that it
2//! contains is determined by a property inside the struct. The content of the
3//! Zval is stored in a union.
4
5use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};
6
7use cfg_if::cfg_if;
8
9use crate::types::ZendIterator;
10use crate::types::iterable::Iterable;
11use crate::{
12    binary::Pack,
13    binary_slice::PackSlice,
14    boxed::ZBox,
15    convert::{FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
16    error::{Error, Result},
17    ffi::{
18        _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, ext_php_rs_zend_string_release,
19        zend_array_dup, zend_is_callable, zend_is_identical, zend_is_iterable, zend_is_true,
20        zend_resource, zend_value, zval, zval_ptr_dtor,
21    },
22    flags::DataType,
23    flags::ZvalTypeFlags,
24    rc::PhpRc,
25    types::{ZendCallable, ZendHashTable, ZendLong, ZendObject, ZendStr},
26};
27
28/// A zend value. This is the primary storage container used throughout the Zend
29/// engine.
30///
31/// A zval can be thought of as a Rust enum, a type that can contain different
32/// values such as integers, strings, objects etc.
33pub type Zval = zval;
34
35// TODO(david): can we make zval send+sync? main problem is that refcounted
36// types do not have atomic refcounters, so technically two threads could
37// reference the same object and attempt to modify refcounter at the same time.
38// need to look into how ZTS works.
39
40// unsafe impl Send for Zval {}
41// unsafe impl Sync for Zval {}
42
43impl Zval {
44    /// Creates a new, empty zval.
45    #[must_use]
46    pub const fn new() -> Self {
47        Self {
48            value: zend_value {
49                ptr: ptr::null_mut(),
50            },
51            #[allow(clippy::used_underscore_items)]
52            u1: _zval_struct__bindgen_ty_1 {
53                type_info: DataType::Null.as_u32(),
54            },
55            #[allow(clippy::used_underscore_items)]
56            u2: _zval_struct__bindgen_ty_2 { next: 0 },
57        }
58    }
59
60    /// Creates a null zval
61    #[must_use]
62    pub fn null() -> Zval {
63        let mut zval = Zval::new();
64        zval.set_null();
65        zval
66    }
67
68    /// Creates a zval containing an empty array.
69    #[must_use]
70    pub fn new_array() -> Zval {
71        let mut zval = Zval::new();
72        zval.set_hashtable(ZendHashTable::new());
73        zval
74    }
75
76    /// Dereference the zval, if it is a reference.
77    #[must_use]
78    pub fn dereference(&self) -> &Self {
79        self.reference().or_else(|| self.indirect()).unwrap_or(self)
80    }
81
82    /// Dereference the zval mutable, if it is a reference.
83    ///
84    /// # Panics
85    ///
86    /// Panics if a mutable reference to the zval is not possible.
87    pub fn dereference_mut(&mut self) -> &mut Self {
88        // TODO: probably more ZTS work is needed here
89        if self.is_reference() {
90            #[allow(clippy::unwrap_used)]
91            return self.reference_mut().unwrap();
92        }
93        if self.is_indirect() {
94            #[allow(clippy::unwrap_used)]
95            return self.indirect_mut().unwrap();
96        }
97        self
98    }
99
100    /// Returns the value of the zval if it is a long.
101    #[must_use]
102    pub fn long(&self) -> Option<ZendLong> {
103        if self.is_long() {
104            Some(unsafe { self.value.lval })
105        } else {
106            None
107        }
108    }
109
110    /// Returns the value of the zval if it is a bool.
111    #[must_use]
112    pub fn bool(&self) -> Option<bool> {
113        if self.is_true() {
114            Some(true)
115        } else if self.is_false() {
116            Some(false)
117        } else {
118            None
119        }
120    }
121
122    /// Returns the value of the zval if it is a double.
123    #[must_use]
124    pub fn double(&self) -> Option<f64> {
125        if self.is_double() {
126            Some(unsafe { self.value.dval })
127        } else {
128            None
129        }
130    }
131
132    /// Returns the value of the zval as a zend string, if it is a string.
133    ///
134    /// Note that this functions output will not be the same as
135    /// [`string()`](#method.string), as this function does not attempt to
136    /// convert other types into a [`String`].
137    #[must_use]
138    pub fn zend_str(&self) -> Option<&ZendStr> {
139        if self.is_string() {
140            unsafe { self.value.str_.as_ref() }
141        } else {
142            None
143        }
144    }
145
146    /// Returns the value of the zval if it is a string.
147    ///
148    /// [`str()`]: #method.str
149    pub fn string(&self) -> Option<String> {
150        self.str().map(ToString::to_string)
151    }
152
153    /// Returns the value of the zval if it is a string.
154    ///
155    /// Note that this functions output will not be the same as
156    /// [`string()`](#method.string), as this function does not attempt to
157    /// convert other types into a [`String`], as it could not pass back a
158    /// [`&str`] in those cases.
159    #[must_use]
160    pub fn str(&self) -> Option<&str> {
161        self.zend_str().and_then(|zs| zs.as_str().ok())
162    }
163
164    /// Returns the value of the zval if it is a string and can be unpacked into
165    /// a vector of a given type. Similar to the [`unpack`] function in PHP,
166    /// except you can only unpack one type.
167    ///
168    /// # Safety
169    ///
170    /// There is no way to tell if the data stored in the string is actually of
171    /// the given type. The results of this function can also differ from
172    /// platform-to-platform due to the different representation of some
173    /// types on different platforms. Consult the [`pack`] function
174    /// documentation for more details.
175    ///
176    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
177    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
178    pub fn binary<T: Pack>(&self) -> Option<Vec<T>> {
179        self.zend_str().map(T::unpack_into)
180    }
181
182    /// Returns the value of the zval if it is a string and can be unpacked into
183    /// a slice of a given type. Similar to the [`unpack`] function in PHP,
184    /// except you can only unpack one type.
185    ///
186    /// This function is similar to [`Zval::binary`] except that a slice is
187    /// returned instead of a vector, meaning the contents of the string is
188    /// not copied.
189    ///
190    /// # Safety
191    ///
192    /// There is no way to tell if the data stored in the string is actually of
193    /// the given type. The results of this function can also differ from
194    /// platform-to-platform due to the different representation of some
195    /// types on different platforms. Consult the [`pack`] function
196    /// documentation for more details.
197    ///
198    /// [`pack`]: https://www.php.net/manual/en/function.pack.php
199    /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
200    pub fn binary_slice<T: PackSlice>(&self) -> Option<&[T]> {
201        self.zend_str().map(T::unpack_into)
202    }
203
204    /// Returns the value of the zval if it is a resource.
205    #[must_use]
206    pub fn resource(&self) -> Option<*mut zend_resource> {
207        // TODO: Can we improve this function? I haven't done much research into
208        // resources so I don't know if this is the optimal way to return this.
209        if self.is_resource() {
210            Some(unsafe { self.value.res })
211        } else {
212            None
213        }
214    }
215
216    /// Returns an immutable reference to the underlying zval hashtable if the
217    /// zval contains an array.
218    #[must_use]
219    pub fn array(&self) -> Option<&ZendHashTable> {
220        if self.is_array() {
221            unsafe { self.value.arr.as_ref() }
222        } else {
223            None
224        }
225    }
226
227    /// Returns a mutable reference to the underlying zval hashtable if the zval
228    /// contains an array.
229    ///
230    /// # Array Separation
231    ///
232    /// PHP arrays use copy-on-write (COW) semantics. Before returning a mutable
233    /// reference, this method checks if the array is shared (refcount > 1) and
234    /// if so, creates a private copy. This is equivalent to PHP's
235    /// `SEPARATE_ARRAY()` macro and prevents the "Assertion failed:
236    /// `zend_gc_refcount` == 1" error that occurs when modifying shared arrays.
237    pub fn array_mut(&mut self) -> Option<&mut ZendHashTable> {
238        if self.is_array() {
239            unsafe {
240                let arr = self.value.arr;
241                let ht = &*arr;
242                if ht.is_immutable() {
243                    self.value.arr = zend_array_dup(arr);
244                } else if (*arr).gc.refcount > 1 {
245                    (*arr).gc.refcount -= 1;
246                    self.value.arr = zend_array_dup(arr);
247                }
248                self.value.arr.as_mut()
249            }
250        } else {
251            None
252        }
253    }
254
255    /// Returns the value of the zval if it is an object.
256    #[must_use]
257    pub fn object(&self) -> Option<&ZendObject> {
258        if self.is_object() {
259            unsafe { self.value.obj.as_ref() }
260        } else {
261            None
262        }
263    }
264
265    /// Returns a mutable reference to the object contained in the [`Zval`], if
266    /// any.
267    pub fn object_mut(&mut self) -> Option<&mut ZendObject> {
268        if self.is_object() {
269            unsafe { self.value.obj.as_mut() }
270        } else {
271            None
272        }
273    }
274
275    /// Attempts to call a method on the object contained in the zval.
276    ///
277    /// # Errors
278    ///
279    /// * Returns an error if the [`Zval`] is not an object.
280    // TODO: Measure this
281    #[allow(clippy::inline_always)]
282    #[inline(always)]
283    pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
284        self.object()
285            .ok_or(Error::Object)?
286            .try_call_method(name, params)
287    }
288
289    /// Returns the value of the zval if it is an internal indirect reference.
290    #[must_use]
291    pub fn indirect(&self) -> Option<&Zval> {
292        if self.is_indirect() {
293            Some(unsafe { &*(self.value.zv.cast::<Zval>()) })
294        } else {
295            None
296        }
297    }
298
299    /// Returns a mutable reference to the zval if it is an internal indirect
300    /// reference.
301    // TODO: Verify if this is safe to use, as it allows mutating the
302    // hashtable while only having a reference to it. #461
303    #[allow(clippy::mut_from_ref)]
304    #[must_use]
305    pub fn indirect_mut(&self) -> Option<&mut Zval> {
306        if self.is_indirect() {
307            Some(unsafe { &mut *(self.value.zv.cast::<Zval>()) })
308        } else {
309            None
310        }
311    }
312
313    /// Returns the value of the zval if it is a reference.
314    #[must_use]
315    pub fn reference(&self) -> Option<&Zval> {
316        if self.is_reference() {
317            Some(&unsafe { self.value.ref_.as_ref() }?.val)
318        } else {
319            None
320        }
321    }
322
323    /// Returns a mutable reference to the underlying zval if it is a reference.
324    pub fn reference_mut(&mut self) -> Option<&mut Zval> {
325        if self.is_reference() {
326            Some(&mut unsafe { self.value.ref_.as_mut() }?.val)
327        } else {
328            None
329        }
330    }
331
332    /// Returns the value of the zval if it is callable.
333    #[must_use]
334    pub fn callable(&self) -> Option<ZendCallable<'_>> {
335        // The Zval is checked if it is callable in the `new` function.
336        ZendCallable::new(self).ok()
337    }
338
339    /// Returns an iterator over the zval if it is traversable.
340    #[must_use]
341    pub fn traversable(&self) -> Option<&mut ZendIterator> {
342        if self.is_traversable() {
343            self.object()?.get_class_entry().get_iterator(self, false)
344        } else {
345            None
346        }
347    }
348
349    /// Returns an iterable over the zval if it is an array or traversable. (is
350    /// iterable)
351    #[must_use]
352    pub fn iterable(&self) -> Option<Iterable<'_>> {
353        if self.is_iterable() {
354            Iterable::from_zval(self)
355        } else {
356            None
357        }
358    }
359
360    /// Returns the value of the zval if it is a pointer.
361    ///
362    /// # Safety
363    ///
364    /// The caller must ensure that the pointer contained in the zval is in fact
365    /// a pointer to an instance of `T`, as the zval has no way of defining
366    /// the type of pointer.
367    #[must_use]
368    pub unsafe fn ptr<T>(&self) -> Option<*mut T> {
369        if self.is_ptr() {
370            Some(unsafe { self.value.ptr.cast::<T>() })
371        } else {
372            None
373        }
374    }
375
376    /// Attempts to call the zval as a callable with a list of arguments to pass
377    /// to the function. Note that a thrown exception inside the callable is
378    /// not detectable, therefore you should check if the return value is
379    /// valid rather than unwrapping. Returns a result containing the return
380    /// value of the function, or an error.
381    ///
382    /// You should not call this function directly, rather through the
383    /// [`call_user_func`] macro.
384    ///
385    /// # Parameters
386    ///
387    /// * `params` - A list of parameters to call the function with.
388    ///
389    /// # Errors
390    ///
391    /// * Returns an error if the [`Zval`] is not callable.
392    // TODO: Measure this
393    #[allow(clippy::inline_always)]
394    #[inline(always)]
395    pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
396        self.callable().ok_or(Error::Callable)?.try_call(params)
397    }
398
399    /// Returns the type of the Zval.
400    #[must_use]
401    pub fn get_type(&self) -> DataType {
402        DataType::from(u32::from(unsafe { self.u1.v.type_ }))
403    }
404
405    /// Returns true if the zval is a long, false otherwise.
406    #[must_use]
407    pub fn is_long(&self) -> bool {
408        self.get_type() == DataType::Long
409    }
410
411    /// Returns true if the zval is null, false otherwise.
412    #[must_use]
413    pub fn is_null(&self) -> bool {
414        self.get_type() == DataType::Null
415    }
416
417    /// Returns true if the zval is true, false otherwise.
418    #[must_use]
419    pub fn is_true(&self) -> bool {
420        self.get_type() == DataType::True
421    }
422
423    /// Returns true if the zval is false, false otherwise.
424    #[must_use]
425    pub fn is_false(&self) -> bool {
426        self.get_type() == DataType::False
427    }
428
429    /// Returns true if the zval is a bool, false otherwise.
430    #[must_use]
431    pub fn is_bool(&self) -> bool {
432        self.is_true() || self.is_false()
433    }
434
435    /// Returns true if the zval is a double, false otherwise.
436    #[must_use]
437    pub fn is_double(&self) -> bool {
438        self.get_type() == DataType::Double
439    }
440
441    /// Returns true if the zval is a string, false otherwise.
442    #[must_use]
443    pub fn is_string(&self) -> bool {
444        self.get_type() == DataType::String
445    }
446
447    /// Returns true if the zval is a resource, false otherwise.
448    #[must_use]
449    pub fn is_resource(&self) -> bool {
450        self.get_type() == DataType::Resource
451    }
452
453    /// Returns true if the zval is an array, false otherwise.
454    #[must_use]
455    pub fn is_array(&self) -> bool {
456        self.get_type() == DataType::Array
457    }
458
459    /// Returns true if the zval is an object, false otherwise.
460    #[must_use]
461    pub fn is_object(&self) -> bool {
462        matches!(self.get_type(), DataType::Object(_))
463    }
464
465    /// Returns true if the zval is a reference, false otherwise.
466    #[must_use]
467    pub fn is_reference(&self) -> bool {
468        self.get_type() == DataType::Reference
469    }
470
471    /// Returns true if the zval is a reference, false otherwise.
472    #[must_use]
473    pub fn is_indirect(&self) -> bool {
474        self.get_type() == DataType::Indirect
475    }
476
477    /// Returns true if the zval is callable, false otherwise.
478    #[must_use]
479    pub fn is_callable(&self) -> bool {
480        let ptr: *const Self = self;
481        unsafe { zend_is_callable(ptr.cast_mut(), 0, std::ptr::null_mut()) }
482    }
483
484    /// Checks if the zval is identical to another one.
485    /// This works like `===` in php.
486    ///
487    /// # Parameters
488    ///
489    /// * `other` - The the zval to check identity against.
490    #[must_use]
491    pub fn is_identical(&self, other: &Self) -> bool {
492        let self_p: *const Self = self;
493        let other_p: *const Self = other;
494        unsafe { zend_is_identical(self_p.cast_mut(), other_p.cast_mut()) }
495    }
496
497    /// Returns true if the zval is traversable, false otherwise.
498    #[must_use]
499    pub fn is_traversable(&self) -> bool {
500        match self.object() {
501            None => false,
502            Some(obj) => obj.is_traversable(),
503        }
504    }
505
506    /// Returns true if the zval is iterable (array or traversable), false
507    /// otherwise.
508    #[must_use]
509    pub fn is_iterable(&self) -> bool {
510        let ptr: *const Self = self;
511        unsafe { zend_is_iterable(ptr.cast_mut()) }
512    }
513
514    /// Returns true if the zval contains a pointer, false otherwise.
515    #[must_use]
516    pub fn is_ptr(&self) -> bool {
517        self.get_type() == DataType::Ptr
518    }
519
520    /// Returns true if the zval is a scalar value (integer, float, string, or
521    /// bool), false otherwise.
522    ///
523    /// This is equivalent to PHP's `is_scalar()` function.
524    #[must_use]
525    pub fn is_scalar(&self) -> bool {
526        matches!(
527            self.get_type(),
528            DataType::Long | DataType::Double | DataType::String | DataType::True | DataType::False
529        )
530    }
531
532    // =========================================================================
533    // Type Coercion Methods
534    // =========================================================================
535    //
536    // These methods convert the zval's value to a different type following
537    // PHP's type coercion rules. Unlike the mutating `coerce_into_*` methods
538    // in some implementations, these are pure functions that return a new value.
539
540    /// Coerces the value to a boolean following PHP's type coercion rules.
541    ///
542    /// This uses PHP's internal `zend_is_true` function to determine the
543    /// boolean value, which handles all PHP types correctly:
544    /// - `null` → `false`
545    /// - `false` → `false`, `true` → `true`
546    /// - `0`, `0.0`, `""`, `"0"` → `false`
547    /// - Empty arrays → `false`
548    /// - Everything else → `true`
549    ///
550    /// # Example
551    ///
552    /// ```no_run
553    /// use ext_php_rs::types::Zval;
554    ///
555    /// let mut zv = Zval::new();
556    /// zv.set_long(0);
557    /// assert_eq!(zv.coerce_to_bool(), false);
558    ///
559    /// zv.set_long(42);
560    /// assert_eq!(zv.coerce_to_bool(), true);
561    /// ```
562    #[must_use]
563    pub fn coerce_to_bool(&self) -> bool {
564        cfg_if! {
565            if #[cfg(php84)] {
566                let ptr: *const Self = self;
567                unsafe { zend_is_true(ptr) }
568            } else {
569                // Pre-PHP 8.4: zend_is_true takes *mut and returns c_int
570                let ptr = self as *const Self as *mut Self;
571                unsafe { zend_is_true(ptr) != 0 }
572            }
573        }
574    }
575
576    /// Coerces the value to a string following PHP's type coercion rules.
577    ///
578    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
579    /// for types that cannot be meaningfully converted to strings (arrays,
580    /// resources, objects without `__toString`).
581    ///
582    /// Conversion rules:
583    /// - Strings → returned as-is
584    /// - Integers → decimal string representation
585    /// - Floats → string representation (uses uppercase `E` for scientific
586    ///   notation when exponent >= 14 or <= -5, matching PHP)
587    /// - `true` → `"1"`, `false` → `""`
588    /// - `null` → `""`
589    /// - `INF` → `"INF"`, `-INF` → `"-INF"`, `NAN` → `"NAN"` (uppercase)
590    /// - Objects with `__toString()` → result of calling `__toString()`
591    /// - Arrays, resources, objects without `__toString()` → `None`
592    ///
593    /// # Example
594    ///
595    /// ```no_run
596    /// use ext_php_rs::types::Zval;
597    ///
598    /// let mut zv = Zval::new();
599    /// zv.set_long(42);
600    /// assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
601    ///
602    /// zv.set_bool(true);
603    /// assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
604    /// ```
605    #[must_use]
606    pub fn coerce_to_string(&self) -> Option<String> {
607        // Already a string
608        if let Some(s) = self.str() {
609            return Some(s.to_string());
610        }
611
612        // Boolean
613        if let Some(b) = self.bool() {
614            return Some(if b { "1".to_string() } else { String::new() });
615        }
616
617        // Null
618        if self.is_null() {
619            return Some(String::new());
620        }
621
622        // Integer
623        if let Some(l) = self.long() {
624            return Some(l.to_string());
625        }
626
627        // Float
628        if let Some(d) = self.double() {
629            // PHP uses uppercase for special float values
630            if d.is_nan() {
631                return Some("NAN".to_string());
632            }
633            if d.is_infinite() {
634                return Some(if d.is_sign_positive() {
635                    "INF".to_string()
636                } else {
637                    "-INF".to_string()
638                });
639            }
640            // PHP uses a format similar to printf's %G with ~14 digits precision
641            // and uppercase E for scientific notation
642            return Some(php_float_to_string(d));
643        }
644
645        // Object with __toString
646        if let Some(obj) = self.object()
647            && let Ok(result) = obj.try_call_method("__toString", vec![])
648        {
649            return result.str().map(ToString::to_string);
650        }
651
652        // Arrays, resources, and objects without __toString cannot be converted
653        None
654    }
655
656    /// Coerces the value to an integer following PHP's type coercion rules.
657    ///
658    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
659    /// for types that cannot be meaningfully converted to integers (arrays,
660    /// resources, objects).
661    ///
662    /// Conversion rules:
663    /// - Integers → returned as-is
664    /// - Floats → truncated toward zero
665    /// - `true` → `1`, `false` → `0`
666    /// - `null` → `0`
667    /// - Strings → parsed as integer (leading numeric portion only; stops at
668    ///   first non-digit; returns 0 if non-numeric; saturates on overflow)
669    /// - Arrays, resources, objects → `None`
670    ///
671    /// # Example
672    ///
673    /// ```no_run
674    /// use ext_php_rs::types::Zval;
675    ///
676    /// let mut zv = Zval::new();
677    /// zv.set_string("42abc", false);
678    /// assert_eq!(zv.coerce_to_long(), Some(42));
679    ///
680    /// zv.set_double(3.7);
681    /// assert_eq!(zv.coerce_to_long(), Some(3));
682    /// ```
683    #[must_use]
684    pub fn coerce_to_long(&self) -> Option<ZendLong> {
685        // Already an integer
686        if let Some(l) = self.long() {
687            return Some(l);
688        }
689
690        // Boolean
691        if let Some(b) = self.bool() {
692            return Some(ZendLong::from(b));
693        }
694
695        // Null
696        if self.is_null() {
697            return Some(0);
698        }
699
700        // Float - truncate toward zero
701        if let Some(d) = self.double() {
702            #[allow(clippy::cast_possible_truncation)]
703            return Some(d as ZendLong);
704        }
705
706        // String - parse leading numeric portion
707        if let Some(s) = self.str() {
708            return Some(parse_long_from_str(s));
709        }
710
711        // Arrays, resources, objects cannot be converted
712        None
713    }
714
715    /// Coerces the value to a float following PHP's type coercion rules.
716    ///
717    /// This is a Rust implementation that matches PHP's behavior. Returns `None`
718    /// for types that cannot be meaningfully converted to floats (arrays,
719    /// resources, objects).
720    ///
721    /// Conversion rules:
722    /// - Floats → returned as-is
723    /// - Integers → converted to float
724    /// - `true` → `1.0`, `false` → `0.0`
725    /// - `null` → `0.0`
726    /// - Strings → parsed as float (leading numeric portion including decimal
727    ///   point and scientific notation; returns 0.0 if non-numeric)
728    /// - Arrays, resources, objects → `None`
729    ///
730    /// # Example
731    ///
732    /// ```no_run
733    /// use ext_php_rs::types::Zval;
734    ///
735    /// let mut zv = Zval::new();
736    /// zv.set_string("3.14abc", false);
737    /// assert_eq!(zv.coerce_to_double(), Some(3.14));
738    ///
739    /// zv.set_long(42);
740    /// assert_eq!(zv.coerce_to_double(), Some(42.0));
741    /// ```
742    #[must_use]
743    pub fn coerce_to_double(&self) -> Option<f64> {
744        // Already a float
745        if let Some(d) = self.double() {
746            return Some(d);
747        }
748
749        // Integer
750        if let Some(l) = self.long() {
751            #[allow(clippy::cast_precision_loss)]
752            return Some(l as f64);
753        }
754
755        // Boolean
756        if let Some(b) = self.bool() {
757            return Some(if b { 1.0 } else { 0.0 });
758        }
759
760        // Null
761        if self.is_null() {
762            return Some(0.0);
763        }
764
765        // String - parse leading numeric portion
766        if let Some(s) = self.str() {
767            return Some(parse_double_from_str(s));
768        }
769
770        // Arrays, resources, objects cannot be converted
771        None
772    }
773
774    /// Sets the value of the zval as a string. Returns nothing in a result when
775    /// successful.
776    ///
777    /// # Parameters
778    ///
779    /// * `val` - The value to set the zval as.
780    /// * `persistent` - Whether the string should persist between requests.
781    ///
782    /// # Persistent Strings
783    ///
784    /// When `persistent` is `true`, the string is allocated from PHP's
785    /// persistent heap (using `malloc`) rather than the request-bound heap.
786    /// This is typically used for strings that need to survive across multiple
787    /// PHP requests, such as class names, function names, or module-level data.
788    ///
789    /// **Important:** The string will still be freed when the Zval is dropped.
790    /// The `persistent` flag only affects which memory allocator is used. If
791    /// you need a string to outlive the Zval, consider using
792    /// [`std::mem::forget`] on the Zval or storing the string elsewhere.
793    ///
794    /// For most use cases (return values, function arguments, temporary
795    /// storage), you should use `persistent: false`.
796    ///
797    /// # Errors
798    ///
799    /// Never returns an error.
800    // TODO: Check if we can drop the result here.
801    pub fn set_string(&mut self, val: &str, persistent: bool) -> Result<()> {
802        self.set_zend_string(ZendStr::new(val, persistent));
803        Ok(())
804    }
805
806    /// Sets the value of the zval as a Zend string.
807    ///
808    /// The Zval takes ownership of the string. When the Zval is dropped,
809    /// the string will be released.
810    ///
811    /// # Parameters
812    ///
813    /// * `val` - String content.
814    pub fn set_zend_string(&mut self, val: ZBox<ZendStr>) {
815        self.change_type(ZvalTypeFlags::StringEx);
816        self.value.str_ = val.into_raw();
817    }
818
819    /// Sets the value of the zval as a binary string, which is represented in
820    /// Rust as a vector.
821    ///
822    /// # Parameters
823    ///
824    /// * `val` - The value to set the zval as.
825    pub fn set_binary<T: Pack>(&mut self, val: Vec<T>) {
826        self.change_type(ZvalTypeFlags::StringEx);
827        let ptr = T::pack_into(val);
828        self.value.str_ = ptr;
829    }
830
831    /// Sets the value of the zval as an interned string. Returns nothing in a
832    /// result when successful.
833    ///
834    /// Interned strings are stored once and are immutable. PHP stores them in
835    /// an internal hashtable. Unlike regular strings, interned strings are not
836    /// reference counted and should not be freed by `zval_ptr_dtor`.
837    ///
838    /// # Parameters
839    ///
840    /// * `val` - The value to set the zval as.
841    /// * `persistent` - Whether the string should persist between requests.
842    ///
843    /// # Errors
844    ///
845    /// Never returns an error.
846    // TODO: Check if we can drop the result here.
847    pub fn set_interned_string(&mut self, val: &str, persistent: bool) -> Result<()> {
848        // Use InternedStringEx (without RefCounted) because interned strings
849        // should not have their refcount modified by zval_ptr_dtor.
850        self.change_type(ZvalTypeFlags::InternedStringEx);
851        self.value.str_ = ZendStr::new_interned(val, persistent).into_raw();
852        Ok(())
853    }
854
855    /// Sets the value of the zval as a long.
856    ///
857    /// # Parameters
858    ///
859    /// * `val` - The value to set the zval as.
860    pub fn set_long<T: Into<ZendLong>>(&mut self, val: T) {
861        self.internal_set_long(val.into());
862    }
863
864    fn internal_set_long(&mut self, val: ZendLong) {
865        self.change_type(ZvalTypeFlags::Long);
866        self.value.lval = val;
867    }
868
869    /// Sets the value of the zval as a double.
870    ///
871    /// # Parameters
872    ///
873    /// * `val` - The value to set the zval as.
874    pub fn set_double<T: Into<f64>>(&mut self, val: T) {
875        self.internal_set_double(val.into());
876    }
877
878    fn internal_set_double(&mut self, val: f64) {
879        self.change_type(ZvalTypeFlags::Double);
880        self.value.dval = val;
881    }
882
883    /// Sets the value of the zval as a boolean.
884    ///
885    /// # Parameters
886    ///
887    /// * `val` - The value to set the zval as.
888    pub fn set_bool<T: Into<bool>>(&mut self, val: T) {
889        self.internal_set_bool(val.into());
890    }
891
892    fn internal_set_bool(&mut self, val: bool) {
893        self.change_type(if val {
894            ZvalTypeFlags::True
895        } else {
896            ZvalTypeFlags::False
897        });
898    }
899
900    /// Sets the value of the zval as null.
901    ///
902    /// This is the default of a zval.
903    pub fn set_null(&mut self) {
904        self.change_type(ZvalTypeFlags::Null);
905    }
906
907    /// Sets the value of the zval as a resource.
908    ///
909    /// # Parameters
910    ///
911    /// * `val` - The value to set the zval as.
912    pub fn set_resource(&mut self, val: *mut zend_resource) {
913        self.change_type(ZvalTypeFlags::ResourceEx);
914        self.value.res = val;
915    }
916
917    /// Sets the value of the zval as a reference to an object.
918    ///
919    /// # Parameters
920    ///
921    /// * `val` - The value to set the zval as.
922    pub fn set_object(&mut self, val: &mut ZendObject) {
923        self.change_type(ZvalTypeFlags::ObjectEx);
924        val.inc_count(); // TODO(david): not sure if this is needed :/
925        self.value.obj = ptr::from_ref(val).cast_mut();
926    }
927
928    /// Sets the value of the zval as an array. Returns nothing in a result on
929    /// success.
930    ///
931    /// # Parameters
932    ///
933    /// * `val` - The value to set the zval as.
934    ///
935    /// # Errors
936    ///
937    /// * Returns an error if the conversion to a hashtable fails.
938    pub fn set_array<T: TryInto<ZBox<ZendHashTable>, Error = Error>>(
939        &mut self,
940        val: T,
941    ) -> Result<()> {
942        self.set_hashtable(val.try_into()?);
943        Ok(())
944    }
945
946    /// Sets the value of the zval as an array. Returns nothing in a result on
947    /// success.
948    ///
949    /// # Parameters
950    ///
951    /// * `val` - The value to set the zval as.
952    pub fn set_hashtable(&mut self, val: ZBox<ZendHashTable>) {
953        // Handle immutable shared arrays (e.g., the empty array) similar to
954        // ZVAL_EMPTY_ARRAY. Immutable arrays should not be reference counted.
955        let type_info = if val.is_immutable() {
956            ZvalTypeFlags::Array
957        } else {
958            ZvalTypeFlags::ArrayEx
959        };
960        self.change_type(type_info);
961        self.value.arr = val.into_raw();
962    }
963
964    /// Sets the value of the zval as a pointer.
965    ///
966    /// # Parameters
967    ///
968    /// * `ptr` - The pointer to set the zval as.
969    pub fn set_ptr<T>(&mut self, ptr: *mut T) {
970        self.u1.type_info = ZvalTypeFlags::Ptr.bits();
971        self.value.ptr = ptr.cast::<c_void>();
972    }
973
974    /// Used to drop the Zval but keep the value of the zval intact.
975    ///
976    /// This is important when copying the value of the zval, as the actual
977    /// value will not be copied, but the pointer to the value (string for
978    /// example) will be copied.
979    pub(crate) fn release(mut self) {
980        // NOTE(david): don't use `change_type` here as we are wanting to keep the
981        // contents intact.
982        self.u1.type_info = ZvalTypeFlags::Null.bits();
983    }
984
985    /// Changes the type of the zval, freeing the current contents when
986    /// applicable.
987    ///
988    /// # Parameters
989    ///
990    /// * `ty` - The new type of the zval.
991    fn change_type(&mut self, ty: ZvalTypeFlags) {
992        // SAFETY: we have exclusive mutable access to this zval so can free the
993        // contents.
994        //
995        // For strings, we use zend_string_release directly instead of zval_ptr_dtor
996        // to correctly handle persistent strings. zend_string_release properly checks
997        // the IS_STR_PERSISTENT flag and uses the correct deallocator (free vs efree).
998        // This fixes heap corruption issues when dropping Zvals containing persistent
999        // strings (see issue #424).
1000        //
1001        // For simple types (Long, Double, Bool, Null, Undef, True, False), we don't
1002        // need to call zval_ptr_dtor because they don't have heap-allocated data.
1003        // This is important for cargo-php stub generation where zval_ptr_dtor is
1004        // stubbed as a null pointer - calling it would crash.
1005        if self.is_string() {
1006            unsafe {
1007                if let Some(str_ptr) = self.value.str_.as_mut() {
1008                    ext_php_rs_zend_string_release(str_ptr);
1009                }
1010            }
1011        } else if self.is_array()
1012            || self.is_object()
1013            || self.is_resource()
1014            || self.is_reference()
1015            || self.get_type() == DataType::ConstantExpression
1016        {
1017            // Only call zval_ptr_dtor for reference-counted types
1018            unsafe { zval_ptr_dtor(self) };
1019        }
1020        // Simple types (Long, Double, Bool, Null, etc.) need no cleanup
1021        self.u1.type_info = ty.bits();
1022    }
1023
1024    /// Extracts some type from a `Zval`.
1025    ///
1026    /// This is a wrapper function around `TryFrom`.
1027    #[must_use]
1028    pub fn extract<'a, T>(&'a self) -> Option<T>
1029    where
1030        T: FromZval<'a>,
1031    {
1032        FromZval::from_zval(self)
1033    }
1034
1035    /// Creates a shallow clone of the [`Zval`].
1036    ///
1037    /// This copies the contents of the [`Zval`], and increments the reference
1038    /// counter of the underlying value (if it is reference counted).
1039    ///
1040    /// For example, if the zval contains a long, it will simply copy the value.
1041    /// However, if the zval contains an object, the new zval will point to the
1042    /// same object, and the objects reference counter will be incremented.
1043    ///
1044    /// # Returns
1045    ///
1046    /// The cloned zval.
1047    #[must_use]
1048    pub fn shallow_clone(&self) -> Zval {
1049        let mut new = Zval::new();
1050        new.u1 = self.u1;
1051        new.value = self.value;
1052
1053        // SAFETY: `u1` union is only used for easier bitmasking. It is valid to read
1054        // from either of the variants.
1055        //
1056        // SAFETY: If the value if refcounted (`self.u1.type_info & Z_TYPE_FLAGS_MASK`)
1057        // then it is valid to dereference `self.value.counted`.
1058        unsafe {
1059            let flags = ZvalTypeFlags::from_bits_retain(self.u1.type_info);
1060            if flags.contains(ZvalTypeFlags::RefCounted) {
1061                (*self.value.counted).gc.refcount += 1;
1062            }
1063        }
1064
1065        new
1066    }
1067}
1068
1069impl Debug for Zval {
1070    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1071        let mut dbg = f.debug_struct("Zval");
1072        let ty = self.get_type();
1073        dbg.field("type", &ty);
1074
1075        macro_rules! field {
1076            ($value: expr) => {
1077                dbg.field("val", &$value)
1078            };
1079        }
1080
1081        match ty {
1082            DataType::Undef | DataType::Null | DataType::ConstantExpression | DataType::Void => {
1083                field!(Option::<()>::None)
1084            }
1085            DataType::False => field!(false),
1086            DataType::True => field!(true),
1087            DataType::Long => field!(self.long()),
1088            DataType::Double => field!(self.double()),
1089            DataType::String | DataType::Mixed | DataType::Callable => field!(self.string()),
1090            DataType::Array => field!(self.array()),
1091            DataType::Object(_) => field!(self.object()),
1092            DataType::Resource => field!(self.resource()),
1093            DataType::Reference => field!(self.reference()),
1094            DataType::Bool => field!(self.bool()),
1095            DataType::Indirect => field!(self.indirect()),
1096            DataType::Iterable => field!(self.iterable()),
1097            // SAFETY: We are not accessing the pointer.
1098            DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
1099        };
1100
1101        dbg.finish()
1102    }
1103}
1104
1105impl Drop for Zval {
1106    fn drop(&mut self) {
1107        self.change_type(ZvalTypeFlags::Null);
1108    }
1109}
1110
1111impl Default for Zval {
1112    fn default() -> Self {
1113        Self::new()
1114    }
1115}
1116
1117impl IntoZval for Zval {
1118    const TYPE: DataType = DataType::Mixed;
1119    const NULLABLE: bool = true;
1120
1121    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1122        *zv = self;
1123        Ok(())
1124    }
1125}
1126
1127impl<'a> FromZval<'a> for &'a Zval {
1128    const TYPE: DataType = DataType::Mixed;
1129
1130    fn from_zval(zval: &'a Zval) -> Option<Self> {
1131        Some(zval)
1132    }
1133}
1134
1135impl<'a> FromZvalMut<'a> for &'a mut Zval {
1136    const TYPE: DataType = DataType::Mixed;
1137
1138    fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
1139        Some(zval)
1140    }
1141}
1142
1143/// Formats a float to a string following PHP's type coercion rules.
1144///
1145/// This is a Rust implementation that matches PHP's behavior. PHP uses a format
1146/// similar to printf's `%G` with 14 significant digits:
1147/// - Uses scientific notation when exponent >= 14 or <= -5
1148/// - Uses uppercase 'E' for the exponent (e.g., "1.0E+52")
1149/// - Removes trailing zeros after the decimal point
1150fn php_float_to_string(d: f64) -> String {
1151    // Use Rust's %G-like formatting: format with precision, uppercase E
1152    // PHP uses ~14 significant digits
1153    let formatted = format!("{d:.14E}");
1154
1155    // Parse the formatted string to clean it up
1156    if let Some(e_pos) = formatted.find('E') {
1157        let mantissa_part = &formatted[..e_pos];
1158        let exp_part = &formatted[e_pos..];
1159
1160        // Parse exponent to decide format
1161        let exp: i32 = exp_part[1..].parse().unwrap_or(0);
1162
1163        // PHP uses scientific notation when exponent >= 14 or <= -5
1164        if exp >= 14 || exp <= -5 {
1165            // Keep scientific notation, but clean up mantissa
1166            let mantissa = mantissa_part.trim_end_matches('0').trim_end_matches('.');
1167
1168            // Ensure mantissa has at least one decimal digit if there's a decimal point
1169            let mantissa = if mantissa.contains('.') && mantissa.ends_with('.') {
1170                format!("{mantissa}0")
1171            } else if !mantissa.contains('.') {
1172                format!("{mantissa}.0")
1173            } else {
1174                mantissa.to_string()
1175            };
1176
1177            // Format exponent with sign
1178            if exp >= 0 {
1179                format!("{mantissa}E+{exp}")
1180            } else {
1181                format!("{mantissa}E{exp}")
1182            }
1183        } else {
1184            // Use decimal notation
1185            let s = d.to_string();
1186            // Clean up trailing zeros
1187            if s.contains('.') {
1188                s.trim_end_matches('0').trim_end_matches('.').to_string()
1189            } else {
1190                s
1191            }
1192        }
1193    } else {
1194        formatted
1195    }
1196}
1197
1198/// Parses an integer from a string following PHP's type coercion rules.
1199///
1200/// This is a Rust implementation that matches PHP's behavior. It extracts the
1201/// leading numeric portion of a string:
1202/// - `"42"` → 42
1203/// - `"42abc"` → 42
1204/// - `"  42"` → 42 (leading whitespace is skipped)
1205/// - `"-42"` → -42
1206/// - `"abc"` → 0
1207/// - `""` → 0
1208/// - Hexadecimal (`"0xFF"`) → 0 (not interpreted, stops at 'x')
1209/// - Octal (`"010"`) → 10 (not interpreted as octal)
1210/// - Overflow saturates to `ZendLong::MAX` or `ZendLong::MIN`
1211fn parse_long_from_str(s: &str) -> ZendLong {
1212    let s = s.trim_start();
1213    let bytes = s.as_bytes();
1214    if bytes.is_empty() {
1215        return 0;
1216    }
1217
1218    let mut i = 0;
1219    let mut is_negative = false;
1220
1221    // Handle optional sign
1222    if bytes[i] == b'-' || bytes[i] == b'+' {
1223        is_negative = bytes[i] == b'-';
1224        i += 1;
1225    }
1226
1227    let digits_start = i;
1228
1229    // Collect digits
1230    while i < bytes.len() && bytes[i].is_ascii_digit() {
1231        i += 1;
1232    }
1233
1234    // No digits found
1235    if i == digits_start {
1236        return 0;
1237    }
1238
1239    // Parse the slice, saturating on overflow (matching PHP behavior)
1240    match s[..i].parse::<ZendLong>() {
1241        Ok(n) => n,
1242        Err(_) => {
1243            // Overflow: saturate to max or min depending on sign
1244            if is_negative {
1245                ZendLong::MIN
1246            } else {
1247                ZendLong::MAX
1248            }
1249        }
1250    }
1251}
1252
1253/// Parses a float from a string following PHP's type coercion rules.
1254///
1255/// This is a Rust implementation that matches PHP's behavior. It extracts the
1256/// leading numeric portion of a string:
1257/// - `"3.14"` → 3.14
1258/// - `"3.14abc"` → 3.14
1259/// - `"  3.14"` → 3.14 (leading whitespace is skipped)
1260/// - `"-3.14"` → -3.14
1261/// - `"1e10"` → 1e10 (scientific notation supported)
1262/// - `".5"` → 0.5 (leading decimal point)
1263/// - `"5."` → 5.0 (trailing decimal point)
1264/// - `"abc"` → 0.0
1265/// - `""` → 0.0
1266fn parse_double_from_str(s: &str) -> f64 {
1267    let s = s.trim_start();
1268    let bytes = s.as_bytes();
1269    if bytes.is_empty() {
1270        return 0.0;
1271    }
1272
1273    let mut i = 0;
1274    let mut has_decimal = false;
1275    let mut has_exponent = false;
1276    let mut has_digits = false;
1277
1278    // Handle optional sign
1279    if bytes[i] == b'-' || bytes[i] == b'+' {
1280        i += 1;
1281    }
1282
1283    // Collect digits, decimal point, and exponent
1284    while i < bytes.len() {
1285        let c = bytes[i];
1286        if c.is_ascii_digit() {
1287            has_digits = true;
1288            i += 1;
1289        } else if c == b'.' && !has_decimal && !has_exponent {
1290            has_decimal = true;
1291            i += 1;
1292        } else if (c == b'e' || c == b'E') && !has_exponent && has_digits {
1293            has_exponent = true;
1294            i += 1;
1295            // Handle optional sign after exponent
1296            if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
1297                i += 1;
1298            }
1299        } else {
1300            break;
1301        }
1302    }
1303
1304    // Parse the slice or return 0.0
1305    s[..i].parse().unwrap_or(0.0)
1306}
1307
1308#[cfg(test)]
1309#[cfg(feature = "embed")]
1310#[allow(clippy::unwrap_used, clippy::approx_constant)]
1311mod tests {
1312    use super::*;
1313    use crate::embed::Embed;
1314
1315    #[test]
1316    fn test_zval_null() {
1317        Embed::run(|| {
1318            let zval = Zval::null();
1319            assert!(zval.is_null());
1320        });
1321    }
1322
1323    #[test]
1324    fn test_is_scalar() {
1325        Embed::run(|| {
1326            // Test scalar types - should return true
1327            let mut zval_long = Zval::new();
1328            zval_long.set_long(42);
1329            assert!(zval_long.is_scalar());
1330
1331            let mut zval_double = Zval::new();
1332            zval_double.set_double(1.5);
1333            assert!(zval_double.is_scalar());
1334
1335            let mut zval_true = Zval::new();
1336            zval_true.set_bool(true);
1337            assert!(zval_true.is_scalar());
1338
1339            let mut zval_false = Zval::new();
1340            zval_false.set_bool(false);
1341            assert!(zval_false.is_scalar());
1342
1343            let mut zval_string = Zval::new();
1344            zval_string
1345                .set_string("hello", false)
1346                .expect("set_string should succeed");
1347            assert!(zval_string.is_scalar());
1348
1349            // Test non-scalar types - should return false
1350            let zval_null = Zval::null();
1351            assert!(!zval_null.is_scalar());
1352
1353            let zval_array = Zval::new_array();
1354            assert!(!zval_array.is_scalar());
1355        });
1356    }
1357
1358    #[test]
1359    fn test_coerce_to_bool() {
1360        Embed::run(|| {
1361            let mut zv = Zval::new();
1362
1363            // === Truthy values ===
1364            // Integers
1365            zv.set_long(42);
1366            assert!(zv.coerce_to_bool(), "(bool)42 should be true");
1367
1368            zv.set_long(1);
1369            assert!(zv.coerce_to_bool(), "(bool)1 should be true");
1370
1371            zv.set_long(-1);
1372            assert!(zv.coerce_to_bool(), "(bool)-1 should be true");
1373
1374            // Floats
1375            zv.set_double(0.1);
1376            assert!(zv.coerce_to_bool(), "(bool)0.1 should be true");
1377
1378            zv.set_double(-0.1);
1379            assert!(zv.coerce_to_bool(), "(bool)-0.1 should be true");
1380
1381            // Strings - non-empty and not "0"
1382            zv.set_string("hello", false).unwrap();
1383            assert!(zv.coerce_to_bool(), "(bool)'hello' should be true");
1384
1385            zv.set_string("1", false).unwrap();
1386            assert!(zv.coerce_to_bool(), "(bool)'1' should be true");
1387
1388            // PHP: (bool)"false" is true (non-empty string)
1389            zv.set_string("false", false).unwrap();
1390            assert!(zv.coerce_to_bool(), "(bool)'false' should be true");
1391
1392            // PHP: (bool)"true" is true
1393            zv.set_string("true", false).unwrap();
1394            assert!(zv.coerce_to_bool(), "(bool)'true' should be true");
1395
1396            // PHP: (bool)" " (space) is true
1397            zv.set_string(" ", false).unwrap();
1398            assert!(zv.coerce_to_bool(), "(bool)' ' should be true");
1399
1400            // PHP: (bool)"00" is true (only exactly "0" is false)
1401            zv.set_string("00", false).unwrap();
1402            assert!(zv.coerce_to_bool(), "(bool)'00' should be true");
1403
1404            zv.set_bool(true);
1405            assert!(zv.coerce_to_bool(), "(bool)true should be true");
1406
1407            // === Falsy values ===
1408            // Integer zero
1409            zv.set_long(0);
1410            assert!(!zv.coerce_to_bool(), "(bool)0 should be false");
1411
1412            // Float zero (both positive and negative)
1413            zv.set_double(0.0);
1414            assert!(!zv.coerce_to_bool(), "(bool)0.0 should be false");
1415
1416            zv.set_double(-0.0);
1417            assert!(!zv.coerce_to_bool(), "(bool)-0.0 should be false");
1418
1419            // Empty string
1420            zv.set_string("", false).unwrap();
1421            assert!(!zv.coerce_to_bool(), "(bool)'' should be false");
1422
1423            // String "0" - the only non-empty falsy string
1424            zv.set_string("0", false).unwrap();
1425            assert!(!zv.coerce_to_bool(), "(bool)'0' should be false");
1426
1427            zv.set_bool(false);
1428            assert!(!zv.coerce_to_bool(), "(bool)false should be false");
1429
1430            // Null
1431            let null_zv = Zval::null();
1432            assert!(!null_zv.coerce_to_bool(), "(bool)null should be false");
1433
1434            // Empty array
1435            let empty_array = Zval::new_array();
1436            assert!(!empty_array.coerce_to_bool(), "(bool)[] should be false");
1437        });
1438    }
1439
1440    #[test]
1441    fn test_coerce_to_string() {
1442        Embed::run(|| {
1443            let mut zv = Zval::new();
1444
1445            // === Integer to string ===
1446            zv.set_long(42);
1447            assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
1448
1449            zv.set_long(-123);
1450            assert_eq!(zv.coerce_to_string(), Some("-123".to_string()));
1451
1452            zv.set_long(0);
1453            assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1454
1455            // === Float to string ===
1456            zv.set_double(3.14);
1457            assert_eq!(zv.coerce_to_string(), Some("3.14".to_string()));
1458
1459            zv.set_double(-3.14);
1460            assert_eq!(zv.coerce_to_string(), Some("-3.14".to_string()));
1461
1462            zv.set_double(0.0);
1463            assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1464
1465            zv.set_double(1.0);
1466            assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1467
1468            zv.set_double(1.5);
1469            assert_eq!(zv.coerce_to_string(), Some("1.5".to_string()));
1470
1471            zv.set_double(100.0);
1472            assert_eq!(zv.coerce_to_string(), Some("100".to_string()));
1473
1474            // Special float values (uppercase like PHP)
1475            zv.set_double(f64::INFINITY);
1476            assert_eq!(zv.coerce_to_string(), Some("INF".to_string()));
1477
1478            zv.set_double(f64::NEG_INFINITY);
1479            assert_eq!(zv.coerce_to_string(), Some("-INF".to_string()));
1480
1481            zv.set_double(f64::NAN);
1482            assert_eq!(zv.coerce_to_string(), Some("NAN".to_string()));
1483
1484            // Scientific notation thresholds (PHP uses E notation at exp >= 14 or <= -5)
1485            // PHP: (string)1e13 -> "10000000000000"
1486            zv.set_double(1e13);
1487            assert_eq!(zv.coerce_to_string(), Some("10000000000000".to_string()));
1488
1489            // PHP: (string)1e14 -> "1.0E+14"
1490            zv.set_double(1e14);
1491            assert_eq!(zv.coerce_to_string(), Some("1.0E+14".to_string()));
1492
1493            // PHP: (string)1e52 -> "1.0E+52"
1494            zv.set_double(1e52);
1495            assert_eq!(zv.coerce_to_string(), Some("1.0E+52".to_string()));
1496
1497            // PHP: (string)0.0001 -> "0.0001"
1498            zv.set_double(0.0001);
1499            assert_eq!(zv.coerce_to_string(), Some("0.0001".to_string()));
1500
1501            // PHP: (string)1e-5 -> "1.0E-5"
1502            zv.set_double(1e-5);
1503            assert_eq!(zv.coerce_to_string(), Some("1.0E-5".to_string()));
1504
1505            // Negative scientific notation
1506            // PHP: (string)(-1e52) -> "-1.0E+52"
1507            zv.set_double(-1e52);
1508            assert_eq!(zv.coerce_to_string(), Some("-1.0E+52".to_string()));
1509
1510            // PHP: (string)(-1e-10) -> "-1.0E-10"
1511            zv.set_double(-1e-10);
1512            assert_eq!(zv.coerce_to_string(), Some("-1.0E-10".to_string()));
1513
1514            // === Boolean to string ===
1515            // PHP: (string)true -> "1"
1516            zv.set_bool(true);
1517            assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1518
1519            // PHP: (string)false -> ""
1520            zv.set_bool(false);
1521            assert_eq!(zv.coerce_to_string(), Some(String::new()));
1522
1523            // === Null to string ===
1524            // PHP: (string)null -> ""
1525            let null_zv = Zval::null();
1526            assert_eq!(null_zv.coerce_to_string(), Some(String::new()));
1527
1528            // === String unchanged ===
1529            zv.set_string("hello", false).unwrap();
1530            assert_eq!(zv.coerce_to_string(), Some("hello".to_string()));
1531
1532            // === Array cannot be converted ===
1533            let arr_zv = Zval::new_array();
1534            assert_eq!(arr_zv.coerce_to_string(), None);
1535        });
1536    }
1537
1538    #[test]
1539    fn test_coerce_to_long() {
1540        Embed::run(|| {
1541            let mut zv = Zval::new();
1542
1543            // === Integer unchanged ===
1544            zv.set_long(42);
1545            assert_eq!(zv.coerce_to_long(), Some(42));
1546
1547            zv.set_long(-42);
1548            assert_eq!(zv.coerce_to_long(), Some(-42));
1549
1550            zv.set_long(0);
1551            assert_eq!(zv.coerce_to_long(), Some(0));
1552
1553            // === Float truncated (towards zero) ===
1554            // PHP: (int)3.14 -> 3
1555            zv.set_double(3.14);
1556            assert_eq!(zv.coerce_to_long(), Some(3));
1557
1558            // PHP: (int)3.99 -> 3
1559            zv.set_double(3.99);
1560            assert_eq!(zv.coerce_to_long(), Some(3));
1561
1562            // PHP: (int)-3.14 -> -3
1563            zv.set_double(-3.14);
1564            assert_eq!(zv.coerce_to_long(), Some(-3));
1565
1566            // PHP: (int)-3.99 -> -3
1567            zv.set_double(-3.99);
1568            assert_eq!(zv.coerce_to_long(), Some(-3));
1569
1570            // PHP: (int)0.0 -> 0
1571            zv.set_double(0.0);
1572            assert_eq!(zv.coerce_to_long(), Some(0));
1573
1574            // PHP: (int)-0.0 -> 0
1575            zv.set_double(-0.0);
1576            assert_eq!(zv.coerce_to_long(), Some(0));
1577
1578            // === Boolean to integer ===
1579            // PHP: (int)true -> 1
1580            zv.set_bool(true);
1581            assert_eq!(zv.coerce_to_long(), Some(1));
1582
1583            // PHP: (int)false -> 0
1584            zv.set_bool(false);
1585            assert_eq!(zv.coerce_to_long(), Some(0));
1586
1587            // === Null to integer ===
1588            // PHP: (int)null -> 0
1589            let null_zv = Zval::null();
1590            assert_eq!(null_zv.coerce_to_long(), Some(0));
1591
1592            // === String to integer (leading numeric portion) ===
1593            // PHP: (int)"123" -> 123
1594            zv.set_string("123", false).unwrap();
1595            assert_eq!(zv.coerce_to_long(), Some(123));
1596
1597            // PHP: (int)"  123" -> 123
1598            zv.set_string("  123", false).unwrap();
1599            assert_eq!(zv.coerce_to_long(), Some(123));
1600
1601            // PHP: (int)"123abc" -> 123
1602            zv.set_string("123abc", false).unwrap();
1603            assert_eq!(zv.coerce_to_long(), Some(123));
1604
1605            // PHP: (int)"abc123" -> 0
1606            zv.set_string("abc123", false).unwrap();
1607            assert_eq!(zv.coerce_to_long(), Some(0));
1608
1609            // PHP: (int)"" -> 0
1610            zv.set_string("", false).unwrap();
1611            assert_eq!(zv.coerce_to_long(), Some(0));
1612
1613            // PHP: (int)"0" -> 0
1614            zv.set_string("0", false).unwrap();
1615            assert_eq!(zv.coerce_to_long(), Some(0));
1616
1617            // PHP: (int)"-0" -> 0
1618            zv.set_string("-0", false).unwrap();
1619            assert_eq!(zv.coerce_to_long(), Some(0));
1620
1621            // PHP: (int)"+123" -> 123
1622            zv.set_string("+123", false).unwrap();
1623            assert_eq!(zv.coerce_to_long(), Some(123));
1624
1625            // PHP: (int)"   -456" -> -456
1626            zv.set_string("   -456", false).unwrap();
1627            assert_eq!(zv.coerce_to_long(), Some(-456));
1628
1629            // PHP: (int)"3.14" -> 3 (stops at decimal point)
1630            zv.set_string("3.14", false).unwrap();
1631            assert_eq!(zv.coerce_to_long(), Some(3));
1632
1633            // PHP: (int)"3.99" -> 3
1634            zv.set_string("3.99", false).unwrap();
1635            assert_eq!(zv.coerce_to_long(), Some(3));
1636
1637            // PHP: (int)"-3.99" -> -3
1638            zv.set_string("-3.99", false).unwrap();
1639            assert_eq!(zv.coerce_to_long(), Some(-3));
1640
1641            // PHP: (int)"abc" -> 0
1642            zv.set_string("abc", false).unwrap();
1643            assert_eq!(zv.coerce_to_long(), Some(0));
1644
1645            // === Array cannot be converted ===
1646            let arr_zv = Zval::new_array();
1647            assert_eq!(arr_zv.coerce_to_long(), None);
1648        });
1649    }
1650
1651    #[test]
1652    fn test_coerce_to_double() {
1653        Embed::run(|| {
1654            let mut zv = Zval::new();
1655
1656            // === Float unchanged ===
1657            zv.set_double(3.14);
1658            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1659
1660            zv.set_double(-3.14);
1661            assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1662
1663            zv.set_double(0.0);
1664            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1665
1666            // === Integer to float ===
1667            // PHP: (float)42 -> 42.0
1668            zv.set_long(42);
1669            assert!((zv.coerce_to_double().unwrap() - 42.0).abs() < f64::EPSILON);
1670
1671            // PHP: (float)-42 -> -42.0
1672            zv.set_long(-42);
1673            assert!((zv.coerce_to_double().unwrap() - (-42.0)).abs() < f64::EPSILON);
1674
1675            // PHP: (float)0 -> 0.0
1676            zv.set_long(0);
1677            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1678
1679            // === Boolean to float ===
1680            // PHP: (float)true -> 1.0
1681            zv.set_bool(true);
1682            assert!((zv.coerce_to_double().unwrap() - 1.0).abs() < f64::EPSILON);
1683
1684            // PHP: (float)false -> 0.0
1685            zv.set_bool(false);
1686            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1687
1688            // === Null to float ===
1689            // PHP: (float)null -> 0.0
1690            let null_zv = Zval::null();
1691            assert!((null_zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1692
1693            // === String to float ===
1694            // PHP: (float)"3.14" -> 3.14
1695            zv.set_string("3.14", false).unwrap();
1696            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1697
1698            // PHP: (float)"  3.14" -> 3.14
1699            zv.set_string("  3.14", false).unwrap();
1700            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1701
1702            // PHP: (float)"3.14abc" -> 3.14
1703            zv.set_string("3.14abc", false).unwrap();
1704            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1705
1706            // PHP: (float)"abc3.14" -> 0.0
1707            zv.set_string("abc3.14", false).unwrap();
1708            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1709
1710            // PHP: (float)"" -> 0.0
1711            zv.set_string("", false).unwrap();
1712            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1713
1714            // PHP: (float)"0" -> 0.0
1715            zv.set_string("0", false).unwrap();
1716            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1717
1718            // PHP: (float)"0.0" -> 0.0
1719            zv.set_string("0.0", false).unwrap();
1720            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1721
1722            // PHP: (float)"+3.14" -> 3.14
1723            zv.set_string("+3.14", false).unwrap();
1724            assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1725
1726            // PHP: (float)"   -3.14" -> -3.14
1727            zv.set_string("   -3.14", false).unwrap();
1728            assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1729
1730            // Scientific notation
1731            // PHP: (float)"1e10" -> 1e10
1732            zv.set_string("1e10", false).unwrap();
1733            assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1734
1735            // PHP: (float)"1E10" -> 1e10
1736            zv.set_string("1E10", false).unwrap();
1737            assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1738
1739            // PHP: (float)"1.5e-3" -> 0.0015
1740            zv.set_string("1.5e-3", false).unwrap();
1741            assert!((zv.coerce_to_double().unwrap() - 0.0015).abs() < f64::EPSILON);
1742
1743            // PHP: (float)".5" -> 0.5
1744            zv.set_string(".5", false).unwrap();
1745            assert!((zv.coerce_to_double().unwrap() - 0.5).abs() < f64::EPSILON);
1746
1747            // PHP: (float)"5." -> 5.0
1748            zv.set_string("5.", false).unwrap();
1749            assert!((zv.coerce_to_double().unwrap() - 5.0).abs() < f64::EPSILON);
1750
1751            // PHP: (float)"abc" -> 0.0
1752            zv.set_string("abc", false).unwrap();
1753            assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1754
1755            // === Array cannot be converted ===
1756            let arr_zv = Zval::new_array();
1757            assert_eq!(arr_zv.coerce_to_double(), None);
1758        });
1759    }
1760
1761    #[test]
1762    fn test_parse_long_from_str() {
1763        use crate::ffi::zend_long as ZendLong;
1764
1765        // Basic cases
1766        assert_eq!(parse_long_from_str("42"), 42);
1767        assert_eq!(parse_long_from_str("0"), 0);
1768        assert_eq!(parse_long_from_str("-42"), -42);
1769        assert_eq!(parse_long_from_str("+42"), 42);
1770
1771        // Leading/trailing content
1772        assert_eq!(parse_long_from_str("42abc"), 42);
1773        assert_eq!(parse_long_from_str("  42"), 42);
1774        assert_eq!(parse_long_from_str("\t\n42"), 42);
1775        assert_eq!(parse_long_from_str("  -42"), -42);
1776        assert_eq!(parse_long_from_str("42.5"), 42); // stops at decimal point
1777
1778        // Non-numeric strings
1779        assert_eq!(parse_long_from_str("abc"), 0);
1780        assert_eq!(parse_long_from_str(""), 0);
1781        assert_eq!(parse_long_from_str("  "), 0);
1782        assert_eq!(parse_long_from_str("-"), 0);
1783        assert_eq!(parse_long_from_str("+"), 0);
1784        assert_eq!(parse_long_from_str("abc123"), 0);
1785
1786        // Edge cases matching PHP behavior:
1787        // - Hexadecimal: PHP's (int)"0xFF" returns 0 (stops at 'x')
1788        assert_eq!(parse_long_from_str("0xFF"), 0);
1789        assert_eq!(parse_long_from_str("0x10"), 0);
1790
1791        // - Octal notation is NOT interpreted, just reads leading digits
1792        assert_eq!(parse_long_from_str("010"), 10); // not 8
1793
1794        // - Binary notation is NOT interpreted
1795        assert_eq!(parse_long_from_str("0b101"), 0);
1796
1797        // Overflow tests - saturates to max/min
1798        assert_eq!(
1799            parse_long_from_str("99999999999999999999999999"),
1800            ZendLong::MAX
1801        );
1802        assert_eq!(
1803            parse_long_from_str("-99999999999999999999999999"),
1804            ZendLong::MIN
1805        );
1806
1807        // Large but valid numbers
1808        assert_eq!(parse_long_from_str("9223372036854775807"), ZendLong::MAX); // i64::MAX
1809        assert_eq!(parse_long_from_str("-9223372036854775808"), ZendLong::MIN); // i64::MIN
1810    }
1811
1812    #[test]
1813    fn test_parse_double_from_str() {
1814        // Basic cases
1815        assert!((parse_double_from_str("3.14") - 3.14).abs() < f64::EPSILON);
1816        assert!((parse_double_from_str("0.0") - 0.0).abs() < f64::EPSILON);
1817        assert!((parse_double_from_str("-3.14") - (-3.14)).abs() < f64::EPSILON);
1818        assert!((parse_double_from_str("+3.14") - 3.14).abs() < f64::EPSILON);
1819        assert!((parse_double_from_str(".5") - 0.5).abs() < f64::EPSILON);
1820        assert!((parse_double_from_str("5.") - 5.0).abs() < f64::EPSILON);
1821
1822        // Scientific notation
1823        assert!((parse_double_from_str("1e10") - 1e10).abs() < 1.0);
1824        assert!((parse_double_from_str("1E10") - 1e10).abs() < 1.0);
1825        assert!((parse_double_from_str("1.5e-3") - 1.5e-3).abs() < f64::EPSILON);
1826        assert!((parse_double_from_str("1.5E+3") - 1500.0).abs() < f64::EPSILON);
1827        assert!((parse_double_from_str("-1.5e2") - (-150.0)).abs() < f64::EPSILON);
1828
1829        // Leading/trailing content
1830        assert!((parse_double_from_str("3.14abc") - 3.14).abs() < f64::EPSILON);
1831        assert!((parse_double_from_str("  3.14") - 3.14).abs() < f64::EPSILON);
1832        assert!((parse_double_from_str("\t\n3.14") - 3.14).abs() < f64::EPSILON);
1833        assert!((parse_double_from_str("1e10xyz") - 1e10).abs() < 1.0);
1834
1835        // Non-numeric strings
1836        assert!((parse_double_from_str("abc") - 0.0).abs() < f64::EPSILON);
1837        assert!((parse_double_from_str("") - 0.0).abs() < f64::EPSILON);
1838        assert!((parse_double_from_str("  ") - 0.0).abs() < f64::EPSILON);
1839        assert!((parse_double_from_str("-") - 0.0).abs() < f64::EPSILON);
1840        assert!((parse_double_from_str("e10") - 0.0).abs() < f64::EPSILON); // no digits before e
1841
1842        // Integer values
1843        assert!((parse_double_from_str("42") - 42.0).abs() < f64::EPSILON);
1844        assert!((parse_double_from_str("-42") - (-42.0)).abs() < f64::EPSILON);
1845
1846        // Edge cases:
1847        // - Hexadecimal: not supported, returns 0
1848        assert!((parse_double_from_str("0xFF") - 0.0).abs() < f64::EPSILON);
1849
1850        // - Multiple decimal points: stops at second
1851        assert!((parse_double_from_str("1.2.3") - 1.2).abs() < f64::EPSILON);
1852
1853        // - Multiple exponents: stops at second e
1854        assert!((parse_double_from_str("1e2e3") - 100.0).abs() < f64::EPSILON);
1855    }
1856
1857    #[test]
1858    fn test_php_float_to_string() {
1859        // Basic decimal values
1860        assert_eq!(php_float_to_string(3.14), "3.14");
1861        assert_eq!(php_float_to_string(42.0), "42");
1862        assert_eq!(php_float_to_string(-3.14), "-3.14");
1863        assert_eq!(php_float_to_string(0.0), "0");
1864
1865        // Large numbers use scientific notation with uppercase E
1866        // PHP: (string)(float)'999...9' -> "1.0E+52"
1867        assert_eq!(php_float_to_string(1e52), "1.0E+52");
1868        assert_eq!(php_float_to_string(1e20), "1.0E+20");
1869        assert_eq!(php_float_to_string(1e14), "1.0E+14");
1870
1871        // Very small numbers also use scientific notation (at exp <= -5)
1872        assert_eq!(php_float_to_string(1e-10), "1.0E-10");
1873        assert_eq!(php_float_to_string(1e-5), "1.0E-5");
1874        assert_eq!(php_float_to_string(0.00001), "1.0E-5");
1875
1876        // Numbers that don't need scientific notation
1877        assert_eq!(php_float_to_string(1e13), "10000000000000");
1878        assert_eq!(php_float_to_string(0.001), "0.001");
1879        assert_eq!(php_float_to_string(0.0001), "0.0001"); // 1e-4 is decimal
1880    }
1881}