Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions baml_language/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@
class _ParseCache<T> {
_data: $rust_type,

//baml:mut_vm
/// (internal) Final SAP operation shared by streams and public SAP.
function _parse_final(self, json: string) -> T throws root.errors.LlmClient {
$rust_io_function
$rust_function
}

//baml:mut_vm
/// (internal) Partial SAP operation used by `ai.stream.Stream`.
function _parse_partial(self, json: string) -> T | _NoYield throws root.errors.LlmClient {
$rust_io_function
$rust_function
}
}

/// (internal) sentinel returned when an incremental parse needs more text.
class _NoYield {
}

//baml:mut_vm
//baml:fallible
/// (internal) Builds a parse cache for `T`.
///
/// A free function rather than a `_ParseCache` static method: a *static*
Expand All @@ -31,7 +35,7 @@ class _NoYield {
/// operand slots the call site never fills. Instance methods on a generic
/// class (`_parse_final`, `_parse_partial`) are threaded correctly.
function _new_parse_cache<T>() -> _ParseCache<T> throws never {
$rust_io_function
$rust_function
}

/// Parse an LLM's raw text output into `T` with Schema-Aligned Parsing.
Expand Down
9 changes: 8 additions & 1 deletion baml_language/crates/baml_builtins2_codegen/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,8 +1579,15 @@ implements<T, E> Modifier<T, E> for Root {
.find(|b| b.path == "baml.http._fetch")
.unwrap();
assert_eq!(http_fetch.throws, throws(&["Io", "Timeout"]));
}

/// Schema-aligned parsing does no IO, so its entry points are VM natives.
#[test]
fn test_sap_parse_is_a_vm_builtin() {
let (vm_builtins, io_builtins, _class_defs) = extract_native_builtins().unwrap();

let sap_final = io_builtins
assert!(!io_builtins.iter().any(|b| b.path.starts_with("baml.sap.")));
let sap_final = vm_builtins
.iter()
.find(|b| b.path == "baml.sap._ParseCache._parse_final")
.unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ class baml.regex.Error <builtin>/baml/ns_regex/regex.
class baml.regex.Regex <builtin>/baml/ns_regex/regex.baml:97
function baml.regex.new <builtin>/baml/ns_regex/regex.baml:244
function baml.regex.escape <builtin>/baml/ns_regex/regex.baml:268
function baml.sap.parse <builtin>/baml/ns_sap/sap.baml:41
function baml.sap.parse_type <builtin>/baml/ns_sap/sap.baml:54
function baml.sap.parse <builtin>/baml/ns_sap/sap.baml:45
function baml.sap.parse_type <builtin>/baml/ns_sap/sap.baml:58
class baml.spawn.CancelToken <builtin>/baml/ns_spawn/spawn.baml:23
interface baml.spawn.Modifier <builtin>/baml/ns_spawn/spawn.baml:60
class baml.spawn.Plan <builtin>/baml/ns_spawn/spawn.baml:78
Expand Down
65 changes: 50 additions & 15 deletions baml_language/crates/baml_compiler2_mir/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,37 @@ pub(crate) fn ty_to_pattern_template_from_resolved_ty(ty: &RuntimeTy) -> Option<
}
}

/// The value an enclosing `match` scrutinizes, for the coarse-tag proofs.
struct MatchScrutinee {
/// The local the match evaluated its scrutinee into, then each snapshot a
/// narrowing arm (`let x: T`) copies it into and tests in its place.
locals: Vec<Local>,
/// The scrutinee's resolved static type.
ty: RuntimeTy,
}

impl MatchScrutinee {
fn new(local: Local, ty: RuntimeTy) -> Self {
Self {
locals: vec![local],
ty,
}
}

/// The scrutinee's static type when `local` holds the scrutinee.
fn ty_held_by(&self, local: Local) -> Option<&RuntimeTy> {
self.locals.contains(&local).then_some(&self.ty)
}

/// Records that `copy` was assigned a copy of `source`: if `source` holds
/// the scrutinee, so does `copy`.
fn record_copy(&mut self, source: Local, copy: Local) {
if self.locals.contains(&source) {
self.locals.push(copy);
}
}
}

/// Flatten a `RuntimeTy` union into its leaf members (recursively), pushing each
/// non-union leaf into `out`. A recursive alias is kept as a single `TypeAlias`
/// leaf — it is not expanded — matching `ResolvedAliases::convert`.
Expand Down Expand Up @@ -1693,14 +1724,14 @@ struct LoweringContext<'db> {
// baml.ops.Compare`). Saved/restored across nested lambdas.
lambda_param_tir_types: FxHashMap<Name, Tir2Ty>,

/// The `(local, resolved static type)` of the value the enclosing `match` is
/// scrutinizing, set for the duration of its arm lowering (save/restored
/// across nested matches). A container arm's runtime type test consults this
/// — guarded on the local matching — to decide whether a coarse `LIST`/`MAP`
/// tag suffices in place of a structural element check (see
/// The value the enclosing `match` is scrutinizing, set for the duration of
/// its arm lowering (save/restored across nested matches). A container
/// arm's runtime type test consults this — guarded on the tested local
/// holding the scrutinee — to decide whether a coarse `LIST`/`MAP` tag
/// suffices in place of a structural element check (see
/// [`parametric_arm_tag_sufficient`]). `None` outside a match, and the local
/// guard keeps `is` / standalone tests element-precise even inside one.
match_scrutinee: Option<(Local, RuntimeTy)>,
match_scrutinee: Option<MatchScrutinee>,

/// Stable values read while testing patterns, keyed by pattern identity.
/// Binding lowering reuses these exact values instead of rereading mutable
Expand Down Expand Up @@ -12507,9 +12538,10 @@ impl<'db> LoweringContext<'db> {
// Expose the scrutinee's static type to each arm's container type test so
// it can decide whether a coarse `LIST`/`MAP` tag suffices; restore the
// enclosing match's scrutinee (if any) once the arms are lowered.
let saved_scrutinee = self
.match_scrutinee
.replace((scrutinee_local, self.expr_ty(scrutinee)));
let saved_scrutinee = self.match_scrutinee.replace(MatchScrutinee::new(
scrutinee_local,
self.expr_ty(scrutinee),
));

let switched = self.try_lower_as_switch(
scrutinee_local,
Expand Down Expand Up @@ -12585,8 +12617,8 @@ impl<'db> LoweringContext<'db> {
let scrutinee_static_ty: Option<RuntimeTy> = self
.match_scrutinee
.as_ref()
.filter(|(local, _)| *local == scrutinee)
.map(|(_, ty)| ty.clone());
.and_then(|match_scrutinee| match_scrutinee.ty_held_by(scrutinee))
.cloned();

// Classify arms: collect (key, arm_index) for int literal, enum
// variant, or type patterns, and check for a trailing wildcard/binding.
Expand Down Expand Up @@ -13208,14 +13240,14 @@ impl<'db> LoweringContext<'db> {
// If this is a parametric arm whose type arguments the emitter would
// otherwise check structurally, but the enclosing match's scrutinee
// statically proves a coarse `LIST`/`MAP`/`FUTURE` tag suffices,
// emit the tag test directly. Guarded on the scrutinee local so an
// emit the tag test directly. Guarded on the tested local holding
// the scrutinee (itself, or a narrowing arm's snapshot of it) so an
// `is` / nested test against a different value stays arg-precise.
if self
.match_scrutinee
.as_ref()
.is_some_and(|(local, scrutinee_ty)| {
*local == scrutinee && parametric_arm_tag_sufficient(&ty, scrutinee_ty)
})
.and_then(|match_scrutinee| match_scrutinee.ty_held_by(scrutinee))
.is_some_and(|scrutinee_ty| parametric_arm_tag_sufficient(&ty, scrutinee_ty))
{
let tag = match &ty {
RuntimeTy::List(..) => baml_type::typetag::LIST,
Expand Down Expand Up @@ -13770,6 +13802,9 @@ impl<'db> LoweringContext<'db> {
Place::local(snapshot),
Rvalue::Use(Operand::Copy(Place::Local(scrutinee))),
);
if let Some(match_scrutinee) = self.match_scrutinee.as_mut() {
match_scrutinee.record_copy(scrutinee, snapshot);
}
self.tested_pattern_values
.insert(self.pat_metadata_key(pat_id), snapshot);
snapshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,70 @@ function json_wrap_tag() -> string {
test "impl_on_recursive_alias_generic_arg_dispatches" {
assert.equal(json_wrap_tag(), "json-wrap")
}

// ─── 1.9 Container arms on a json scrutinee ──────────────────────────────────

// `json[]` and `map<string, json>` are json's only list and map members, so a
// match on a json value tests these arms by the runtime tag alone. Every kind
// of json value must still reach its own arm, also through recursion (the
// request builders prune nulls this way).

function json_kind(j: json) -> string {
match (j) {
let object: map<string, json> => "map",
let items: json[] => "list",
_ => "scalar",
}
}

function json_kinds() -> string[] {
[
json_kind({"a": 1}),
json_kind({}),
json_kind([1, "two"]),
json_kind([]),
json_kind("map"),
json_kind(1),
json_kind(true),
json_kind(null),
]
}

test "json_container_arms_match_each_kind" {
assert.equal(json_kinds(), ["map", "map", "list", "list", "scalar", "scalar", "scalar", "scalar"])
}

function drop_nulls(j: json) -> json {
match (j) {
let object: map<string, json> => {
let out: map<string, json> = {};
for (let key in object.keys()) {
let entry = object.get(key);
match (entry) {
null => null,
_ => {
let _ = out.set(key, drop_nulls(entry));
null
},
};
}
out
},
let items: json[] => {
let out: json[] = [];
for (let item in items) {
out.push(drop_nulls(item));
}
out
},
_ => j,
}
}

function drop_nulls_nested() -> string {
baml.json.stringify(drop_nulls(baml.json.parse("{\"a\":null,\"b\":[1,{\"c\":null,\"d\":\"x\"}],\"e\":{\"f\":null}}")))
}

test "json_container_arms_recurse_through_nested_values" {
assert.equal(drop_nulls_nested(), "{\"b\":[1,{\"d\":\"x\"}],\"e\":{}}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,49 @@ test "sap_parse_matches_the_enum_lines_the_prompt_renders" {
assert.is_true(prompt.includes("- A: eaten in the morning"));
assert.is_true(prompt.includes("- B: LUNCH eaten at noon"))
}

// ─── @skip fields ────────────────────────────────────────────────────────────
//
// The parser never reads a `@skip` field (the prompt does not show it), so a
// parsed instance holds its type's empty value: "" for a string, the zero of a
// number, false, an empty list, null for an optional field, an enum's first
// variant, and a class of its fields' empty values.

enum ParseCompanionsSkipColor {
Red,
Green,
}

class ParseCompanionsSkipInner {
label: string,
count: int,
}

class ParseCompanionsSkipPerson {
name: string,
age: int @skip,
score: float @skip,
active: bool @skip,
tags: string[] @skip,
nick: string? @skip,
color: ParseCompanionsSkipColor @skip,
inner: ParseCompanionsSkipInner @skip,
}

function parse_companions_skipped_fields_fn() -> bool {
let parsed = baml.sap.parse<ParseCompanionsSkipPerson>("{\"name\": \"Ada\", \"age\": 37}");
parsed == ParseCompanionsSkipPerson {
name: "Ada",
age: 0,
score: 0.0,
active: false,
tags: [],
nick: null,
color: ParseCompanionsSkipColor.Red,
inner: ParseCompanionsSkipInner { label: "", count: 0 },
}
}

test "sap_parse_fills_skipped_fields_with_empty_values" {
assert.is_true(parse_companions_skipped_fields_fn())
}
Loading
Loading