Skip to content

Commit 0c88708

Browse files
committed
Coderabbit fixes
1 parent ee1c18e commit 0c88708

26 files changed

Lines changed: 1052 additions & 214 deletions

File tree

baml_language/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

baml_language/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ web-time = "1.1.0"
265265
getrandom = { version = "0.2" }
266266
getrandom_03 = { package = "getrandom", version = "0.3", features = [ "wasm_js" ] }
267267
num-bigint = { version = "0.4", features = [ "rand", "serde" ] }
268+
num-traits = { version = "0.2" }
268269
rsa = { version = "0.9", default-features = false, features = [ "std", "pem" ] }
269270
sha2 = { version = "0.10", features = [ "oid" ] }
270271

baml_language/crates/baml_builtins2/baml_std/baml/ns_errors/errors.baml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ class HostCallable {
5555
class_name string
5656
language string
5757
traceback string?
58-
// Opaque slot reserved for the bridge SDK's same-host rehydration
59-
// handle. When set by the originating host, the BAML→host decoder on
60-
// the same runtime can resolve it back to the original native
61-
// exception object; foreign runtimes / released keys see `null` and
62-
// fall back to the metadata fields above. Not yet populated end-to-end
63-
// (HostValueKind::Error wiring is pending).
58+
// Opaque slot carrying the bridge SDK's same-host rehydration handle.
59+
// Always populated on the wire by the originating bridge — a `HostCallable`
60+
// with no `_handle` is not a valid BAML value. The originating bridge
61+
// can resolve the handle back to the original native exception object
62+
// (same-process round-trip → `raised === caught` identity); foreign
63+
// runtimes receive the same handle on the wire but their local lookup
64+
// misses, at which point the host SDK degrades to constructing an
65+
// exception from the metadata fields above. The fallback is a
66+
// *lookup-result* concern on the host side, not a wire shape — the
67+
// field itself is required.
6468
_handle $rust_type
6569
}
6670

baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class errors.NotImplemented <builtin>/baml/ns_errors/error
2727
class errors.LlmClient <builtin>/baml/ns_errors/errors.baml:44
2828
class errors.DevOther <builtin>/baml/ns_errors/errors.baml:49
2929
class errors.HostCallable <builtin>/baml/ns_errors/errors.baml:53
30-
class errors.GenericSdkError <builtin>/baml/ns_errors/errors.baml:70
31-
class errors.CompilationError <builtin>/baml/ns_errors/errors.baml:76
30+
class errors.GenericSdkError <builtin>/baml/ns_errors/errors.baml:74
31+
class errors.CompilationError <builtin>/baml/ns_errors/errors.baml:80
3232
class errors.StackFrame <builtin>/baml/ns_errors/stack_trace.baml:2
3333
class errors.StackTrace <builtin>/baml/ns_errors/stack_trace.baml:9
3434
function events.send <builtin>/baml/ns_events/events.baml:3

baml_language/crates/bex_engine/src/conversion.rs

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

baml_language/crates/bex_engine/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,12 @@ pub(crate) fn op_error_to_throw_value(
602602
/// [`Ty::is_subtype_of`] — `BuiltinUnknown` accepts everything (the
603603
/// "throws unknown" fallback for undeclared host contracts); concrete
604604
/// classes reject anything not in their subtype lattice.
605+
///
606+
/// Panic-class values (`baml.panics.*`) bypass the contract entirely:
607+
/// panics are not catchable errors and a fn's `throws E` clause never
608+
/// includes them. This also avoids re-wrapping an engine-generated
609+
/// `HostContractViolation` (from a wrong-return-type check upstream)
610+
/// into a second `HostContractViolation` with a corrupted message.
605611
fn enforce_host_throw_contract(
606612
thread: &mut ActiveHeapPermit<BexThread>,
607613
value: Value,
@@ -613,6 +619,14 @@ fn enforce_host_throw_contract(
613619
return value;
614620
}
615621
let runtime_ty = value_runtime_baml_ty(value, thread.proof());
622+
// Panics propagate as panics regardless of `E` — they're an
623+
// engine-level failure mode, not something the user's callable opts
624+
// into via `throws`.
625+
if let Some(Ty::Class(name, _, _)) = runtime_ty.as_ref()
626+
&& name.is_panic_type()
627+
{
628+
return value;
629+
}
616630
let on_contract = runtime_ty
617631
.as_ref()
618632
.is_some_and(|rt: &Ty| rt.is_subtype_of(contract));

0 commit comments

Comments
 (0)