@@ -1535,3 +1535,268 @@ pub fn test_arg_to_external(v: &bex_vm_types::TestArgValue) -> BexExternalValue
15351535 } ,
15361536 }
15371537}
1538+
1539+ #[ cfg( test) ]
1540+ mod peel_to_rust_type_tests {
1541+ use baml_type:: { TyAttr , TypeName } ;
1542+
1543+ use super :: * ;
1544+
1545+ /// `Ty::Opaque("baml.rust.RustType", _)` — the canonical shape.
1546+ fn rust_type ( ) -> Ty {
1547+ Ty :: Opaque (
1548+ TypeName :: from_dotted_path ( "baml.rust.RustType" ) ,
1549+ TyAttr :: default ( ) ,
1550+ )
1551+ }
1552+
1553+ #[ test]
1554+ fn direct_rust_type_matches ( ) {
1555+ assert_eq ! ( peel_to_rust_type( & rust_type( ) ) , Some ( ( ) ) ) ;
1556+ }
1557+
1558+ #[ test]
1559+ fn optional_rust_type_peels_through ( ) {
1560+ // `Ty::optional(RustType)` lowers to `Ty::Union([RustType, Null])`
1561+ // post-`Ty::Optional`-removal; the union arm in `peel_to_rust_type`
1562+ // picks the single RustType member.
1563+ let ty = Ty :: optional ( rust_type ( ) ) ;
1564+ assert_eq ! ( peel_to_rust_type( & ty) , Some ( ( ) ) ) ;
1565+ }
1566+
1567+ #[ test]
1568+ fn nested_optional_rust_type_peels_through ( ) {
1569+ // `T??` collapses to `T?` per `Ty::optional`'s idempotence rule
1570+ // (a union already containing `null` is returned unchanged), so
1571+ // this is effectively the same shape as the single-optional case
1572+ // — still a single non-null member that peels.
1573+ let ty = Ty :: optional ( Ty :: optional ( rust_type ( ) ) ) ;
1574+ assert_eq ! ( peel_to_rust_type( & ty) , Some ( ( ) ) ) ;
1575+ }
1576+
1577+ #[ test]
1578+ fn singleton_union_with_rust_type_and_null_matches ( ) {
1579+ // `RustType | null` — only one non-null arm so the peel
1580+ // unambiguously picks `RustType`.
1581+ let ty = Ty :: Union (
1582+ vec ! [
1583+ rust_type( ) ,
1584+ Ty :: Null {
1585+ attr: TyAttr :: default ( ) ,
1586+ } ,
1587+ ] ,
1588+ TyAttr :: default ( ) ,
1589+ ) ;
1590+ assert_eq ! ( peel_to_rust_type( & ty) , Some ( ( ) ) ) ;
1591+ }
1592+
1593+ #[ test]
1594+ fn union_with_rust_type_plus_non_rust_arm_still_unique_matches ( ) {
1595+ // `RustType | string` — there's still exactly one `RustType` arm,
1596+ // and `peel_to_rust_type` only cares about uniqueness of *that*
1597+ // shape (non-`RustType` arms count as "doesn't match" and don't
1598+ // contribute to the duplicate-count).
1599+ let ty = Ty :: Union (
1600+ vec ! [
1601+ rust_type( ) ,
1602+ Ty :: String {
1603+ attr: TyAttr :: default ( ) ,
1604+ } ,
1605+ ] ,
1606+ TyAttr :: default ( ) ,
1607+ ) ;
1608+ assert_eq ! ( peel_to_rust_type( & ty) , Some ( ( ) ) ) ;
1609+ }
1610+
1611+ #[ test]
1612+ fn union_with_two_rust_type_arms_is_ambiguous ( ) {
1613+ // `RustType | RustType` — two arms peel to the target. The
1614+ // function rejects to avoid silently picking one.
1615+ let ty = Ty :: Union ( vec ! [ rust_type( ) , rust_type( ) ] , TyAttr :: default ( ) ) ;
1616+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1617+ }
1618+
1619+ #[ test]
1620+ fn plain_string_does_not_match ( ) {
1621+ assert_eq ! (
1622+ peel_to_rust_type( & Ty :: String {
1623+ attr: TyAttr :: default ( )
1624+ } ) ,
1625+ None ,
1626+ ) ;
1627+ }
1628+
1629+ #[ test]
1630+ fn unrelated_opaque_does_not_match ( ) {
1631+ // A different opaque type — e.g. `baml.llm.PromptAst` — must
1632+ // not be confused with `baml.rust.RustType`.
1633+ let ty = Ty :: Opaque (
1634+ TypeName :: from_dotted_path ( "baml.llm.PromptAst" ) ,
1635+ TyAttr :: default ( ) ,
1636+ ) ;
1637+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1638+ }
1639+
1640+ #[ test]
1641+ fn optional_of_unrelated_type_does_not_match ( ) {
1642+ let ty = Ty :: optional ( Ty :: String {
1643+ attr : TyAttr :: default ( ) ,
1644+ } ) ;
1645+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1646+ }
1647+
1648+ #[ test]
1649+ fn union_with_no_rust_type_arm_does_not_match ( ) {
1650+ let ty = Ty :: Union (
1651+ vec ! [
1652+ Ty :: String {
1653+ attr: TyAttr :: default ( ) ,
1654+ } ,
1655+ Ty :: Int {
1656+ attr: TyAttr :: default ( ) ,
1657+ } ,
1658+ ] ,
1659+ TyAttr :: default ( ) ,
1660+ ) ;
1661+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1662+ }
1663+ }
1664+
1665+ #[ cfg( test) ]
1666+ mod peel_function_ty_tests {
1667+ use baml_type:: TyAttr ;
1668+
1669+ use super :: * ;
1670+
1671+ /// `(int) -> string` — the canonical concrete function shape.
1672+ fn fn_ty ( ) -> Ty {
1673+ Ty :: Function {
1674+ params : vec ! [ Ty :: Int {
1675+ attr: TyAttr :: default ( ) ,
1676+ } ] ,
1677+ ret : Box :: new ( Ty :: String {
1678+ attr : TyAttr :: default ( ) ,
1679+ } ) ,
1680+ throws : Box :: new ( Ty :: Void {
1681+ attr : TyAttr :: default ( ) ,
1682+ } ) ,
1683+ attr : TyAttr :: default ( ) ,
1684+ }
1685+ }
1686+
1687+ /// A second, distinct function shape — `() -> int` — used to verify the
1688+ /// uniqueness rule rejects two function members in a union.
1689+ fn other_fn_ty ( ) -> Ty {
1690+ Ty :: Function {
1691+ params : vec ! [ ] ,
1692+ ret : Box :: new ( Ty :: Int {
1693+ attr : TyAttr :: default ( ) ,
1694+ } ) ,
1695+ throws : Box :: new ( Ty :: Void {
1696+ attr : TyAttr :: default ( ) ,
1697+ } ) ,
1698+ attr : TyAttr :: default ( ) ,
1699+ }
1700+ }
1701+
1702+ #[ test]
1703+ fn direct_function_returns_itself ( ) {
1704+ let ty = fn_ty ( ) ;
1705+ let peeled = peel_function_ty ( & ty) . expect ( "must peel a direct Function" ) ;
1706+ assert ! ( matches!( peeled, Ty :: Function { .. } ) ) ;
1707+ }
1708+
1709+ #[ test]
1710+ fn optional_function_peels_through ( ) {
1711+ // `Ty::optional(fn)` lowers to `Ty::Union([fn, Null])`; the union
1712+ // arm in `peel_function_ty` picks the single function member.
1713+ let ty = Ty :: optional ( fn_ty ( ) ) ;
1714+ let peeled = peel_function_ty ( & ty) . expect ( "Union<fn, Null> must peel" ) ;
1715+ assert ! ( matches!( peeled, Ty :: Function { .. } ) ) ;
1716+ }
1717+
1718+ #[ test]
1719+ fn nested_optional_function_peels_through ( ) {
1720+ // `Ty::optional` is idempotent — `T??` collapses to `T?` — so this
1721+ // is effectively the same shape as the single-optional case.
1722+ let ty = Ty :: optional ( Ty :: optional ( fn_ty ( ) ) ) ;
1723+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1724+ }
1725+
1726+ #[ test]
1727+ fn union_with_single_function_arm_peels_through ( ) {
1728+ // `((int) -> string) | null` — only one function member.
1729+ let ty = Ty :: Union (
1730+ vec ! [
1731+ fn_ty( ) ,
1732+ Ty :: Null {
1733+ attr: TyAttr :: default ( ) ,
1734+ } ,
1735+ ] ,
1736+ TyAttr :: default ( ) ,
1737+ ) ;
1738+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1739+ }
1740+
1741+ #[ test]
1742+ fn union_with_function_plus_non_function_arm_peels_through ( ) {
1743+ // `((int) -> string) | string` — exactly one function member.
1744+ let ty = Ty :: Union (
1745+ vec ! [
1746+ fn_ty( ) ,
1747+ Ty :: String {
1748+ attr: TyAttr :: default ( ) ,
1749+ } ,
1750+ ] ,
1751+ TyAttr :: default ( ) ,
1752+ ) ;
1753+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1754+ }
1755+
1756+ #[ test]
1757+ fn union_with_two_distinct_function_arms_is_ambiguous ( ) {
1758+ // `((int) -> string) | (() -> int)` — two function members.
1759+ // The peel rejects to avoid silently picking one. Pins the
1760+ // determinism contract of the helper.
1761+ let ty = Ty :: Union ( vec ! [ fn_ty( ) , other_fn_ty( ) ] , TyAttr :: default ( ) ) ;
1762+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1763+ }
1764+
1765+ #[ test]
1766+ fn plain_string_does_not_match ( ) {
1767+ let ty = Ty :: String {
1768+ attr : TyAttr :: default ( ) ,
1769+ } ;
1770+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1771+ }
1772+
1773+ #[ test]
1774+ fn optional_of_non_function_does_not_match ( ) {
1775+ let ty = Ty :: optional ( Ty :: String {
1776+ attr : TyAttr :: default ( ) ,
1777+ } ) ;
1778+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1779+ }
1780+
1781+ #[ test]
1782+ fn union_with_no_function_arm_does_not_match ( ) {
1783+ let ty = Ty :: Union (
1784+ vec ! [
1785+ Ty :: String {
1786+ attr: TyAttr :: default ( ) ,
1787+ } ,
1788+ Ty :: Int {
1789+ attr: TyAttr :: default ( ) ,
1790+ } ,
1791+ ] ,
1792+ TyAttr :: default ( ) ,
1793+ ) ;
1794+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1795+ }
1796+
1797+ #[ test]
1798+ fn empty_union_does_not_match ( ) {
1799+ let ty = Ty :: Union ( vec ! [ ] , TyAttr :: default ( ) ) ;
1800+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1801+ }
1802+ }
0 commit comments