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