@@ -1539,3 +1539,270 @@ pub fn test_arg_to_external(v: &bex_vm_types::TestArgValue) -> BexExternalValue
15391539 } ,
15401540 }
15411541}
1542+
1543+ #[ cfg( test) ]
1544+ mod peel_to_rust_type_tests {
1545+ use baml_type:: { TyAttr , TypeName } ;
1546+
1547+ use super :: * ;
1548+
1549+ /// `Ty::Opaque("baml.rust.RustType", _)` — the canonical shape.
1550+ fn rust_type ( ) -> Ty {
1551+ Ty :: Opaque (
1552+ TypeName :: from_dotted_path ( "baml.rust.RustType" ) ,
1553+ TyAttr :: default ( ) ,
1554+ )
1555+ }
1556+
1557+ #[ test]
1558+ fn direct_rust_type_matches ( ) {
1559+ assert_eq ! ( peel_to_rust_type( & rust_type( ) ) , Some ( ( ) ) ) ;
1560+ }
1561+
1562+ #[ test]
1563+ fn optional_rust_type_peels_through ( ) {
1564+ let ty = Ty :: Optional ( Box :: new ( rust_type ( ) ) , TyAttr :: default ( ) ) ;
1565+ assert_eq ! ( peel_to_rust_type( & ty) , Some ( ( ) ) ) ;
1566+ }
1567+
1568+ #[ test]
1569+ fn nested_optional_rust_type_peels_through ( ) {
1570+ let ty = Ty :: Optional (
1571+ Box :: new ( Ty :: Optional ( Box :: new ( rust_type ( ) ) , TyAttr :: default ( ) ) ) ,
1572+ TyAttr :: default ( ) ,
1573+ ) ;
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 (
1643+ Box :: new ( Ty :: String {
1644+ attr : TyAttr :: default ( ) ,
1645+ } ) ,
1646+ TyAttr :: default ( ) ,
1647+ ) ;
1648+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1649+ }
1650+
1651+ #[ test]
1652+ fn union_with_no_rust_type_arm_does_not_match ( ) {
1653+ let ty = Ty :: Union (
1654+ vec ! [
1655+ Ty :: String {
1656+ attr: TyAttr :: default ( ) ,
1657+ } ,
1658+ Ty :: Int {
1659+ attr: TyAttr :: default ( ) ,
1660+ } ,
1661+ ] ,
1662+ TyAttr :: default ( ) ,
1663+ ) ;
1664+ assert_eq ! ( peel_to_rust_type( & ty) , None ) ;
1665+ }
1666+ }
1667+
1668+ #[ cfg( test) ]
1669+ mod peel_function_ty_tests {
1670+ use baml_type:: TyAttr ;
1671+
1672+ use super :: * ;
1673+
1674+ /// `(int) -> string` — the canonical concrete function shape.
1675+ fn fn_ty ( ) -> Ty {
1676+ Ty :: Function {
1677+ params : vec ! [ Ty :: Int {
1678+ attr: TyAttr :: default ( ) ,
1679+ } ] ,
1680+ ret : Box :: new ( Ty :: String {
1681+ attr : TyAttr :: default ( ) ,
1682+ } ) ,
1683+ throws : Box :: new ( Ty :: Void {
1684+ attr : TyAttr :: default ( ) ,
1685+ } ) ,
1686+ attr : TyAttr :: default ( ) ,
1687+ }
1688+ }
1689+
1690+ /// A second, distinct function shape — `() -> int` — used to verify the
1691+ /// uniqueness rule rejects two function members in a union.
1692+ fn other_fn_ty ( ) -> Ty {
1693+ Ty :: Function {
1694+ params : vec ! [ ] ,
1695+ ret : Box :: new ( Ty :: Int {
1696+ attr : TyAttr :: default ( ) ,
1697+ } ) ,
1698+ throws : Box :: new ( Ty :: Void {
1699+ attr : TyAttr :: default ( ) ,
1700+ } ) ,
1701+ attr : TyAttr :: default ( ) ,
1702+ }
1703+ }
1704+
1705+ #[ test]
1706+ fn direct_function_returns_itself ( ) {
1707+ let ty = fn_ty ( ) ;
1708+ let peeled = peel_function_ty ( & ty) . expect ( "must peel a direct Function" ) ;
1709+ assert ! ( matches!( peeled, Ty :: Function { .. } ) ) ;
1710+ }
1711+
1712+ #[ test]
1713+ fn optional_function_peels_through ( ) {
1714+ let ty = Ty :: Optional ( Box :: new ( fn_ty ( ) ) , TyAttr :: default ( ) ) ;
1715+ let peeled = peel_function_ty ( & ty) . expect ( "Optional<Function> must peel" ) ;
1716+ assert ! ( matches!( peeled, Ty :: Function { .. } ) ) ;
1717+ }
1718+
1719+ #[ test]
1720+ fn nested_optional_function_peels_through ( ) {
1721+ // `((int) -> string)??` — two layers of `Optional`.
1722+ let ty = Ty :: Optional (
1723+ Box :: new ( Ty :: Optional ( Box :: new ( fn_ty ( ) ) , TyAttr :: default ( ) ) ) ,
1724+ TyAttr :: default ( ) ,
1725+ ) ;
1726+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1727+ }
1728+
1729+ #[ test]
1730+ fn union_with_single_function_arm_peels_through ( ) {
1731+ // `((int) -> string) | null` — only one function member.
1732+ let ty = Ty :: Union (
1733+ vec ! [
1734+ fn_ty( ) ,
1735+ Ty :: Null {
1736+ attr: TyAttr :: default ( ) ,
1737+ } ,
1738+ ] ,
1739+ TyAttr :: default ( ) ,
1740+ ) ;
1741+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1742+ }
1743+
1744+ #[ test]
1745+ fn union_with_function_plus_non_function_arm_peels_through ( ) {
1746+ // `((int) -> string) | string` — exactly one function member.
1747+ let ty = Ty :: Union (
1748+ vec ! [
1749+ fn_ty( ) ,
1750+ Ty :: String {
1751+ attr: TyAttr :: default ( ) ,
1752+ } ,
1753+ ] ,
1754+ TyAttr :: default ( ) ,
1755+ ) ;
1756+ assert ! ( peel_function_ty( & ty) . is_some( ) ) ;
1757+ }
1758+
1759+ #[ test]
1760+ fn union_with_two_distinct_function_arms_is_ambiguous ( ) {
1761+ // `((int) -> string) | (() -> int)` — two function members.
1762+ // The peel rejects to avoid silently picking one. Pins the
1763+ // determinism contract of the helper.
1764+ let ty = Ty :: Union ( vec ! [ fn_ty( ) , other_fn_ty( ) ] , TyAttr :: default ( ) ) ;
1765+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1766+ }
1767+
1768+ #[ test]
1769+ fn plain_string_does_not_match ( ) {
1770+ let ty = Ty :: String {
1771+ attr : TyAttr :: default ( ) ,
1772+ } ;
1773+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1774+ }
1775+
1776+ #[ test]
1777+ fn optional_of_non_function_does_not_match ( ) {
1778+ let ty = Ty :: Optional (
1779+ Box :: new ( Ty :: String {
1780+ attr : TyAttr :: default ( ) ,
1781+ } ) ,
1782+ TyAttr :: default ( ) ,
1783+ ) ;
1784+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1785+ }
1786+
1787+ #[ test]
1788+ fn union_with_no_function_arm_does_not_match ( ) {
1789+ let ty = Ty :: Union (
1790+ vec ! [
1791+ Ty :: String {
1792+ attr: TyAttr :: default ( ) ,
1793+ } ,
1794+ Ty :: Int {
1795+ attr: TyAttr :: default ( ) ,
1796+ } ,
1797+ ] ,
1798+ TyAttr :: default ( ) ,
1799+ ) ;
1800+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1801+ }
1802+
1803+ #[ test]
1804+ fn empty_union_does_not_match ( ) {
1805+ let ty = Ty :: Union ( vec ! [ ] , TyAttr :: default ( ) ) ;
1806+ assert ! ( peel_function_ty( & ty) . is_none( ) ) ;
1807+ }
1808+ }
0 commit comments