From 40e1094eaa37fb38575f55320b35d8e9c0b2ad02 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Sun, 19 Oct 2025 19:43:43 -0700 Subject: [PATCH 01/16] watch x = 1 --- engine/baml-compiler/src/hir/lowering.rs | 13 +- .../baml-compiler/src/watch/watch_options.rs | 72 ++-------- engine/baml-lib/ast/src/ast.rs | 2 +- engine/baml-lib/ast/src/ast/stmt.rs | 73 ++-------- engine/baml-lib/ast/src/parser/datamodel.pest | 30 ++-- engine/baml-lib/ast/src/parser/parse_expr.rs | 128 +----------------- .../baml-lib/baml/tests/hir_files/emit.baml | 8 +- .../expr/emit_missing_value.baml | 12 +- .../validation_files/expr/emit_nested.baml | 6 +- 9 files changed, 58 insertions(+), 286 deletions(-) diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 6c70be1a7b..8c7a907216 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -414,14 +414,19 @@ fn lower_stmt(stmt: &ast::Stmt) -> Statement { expr, span, annotations: _, - watch, + is_watched, }) => { let lifted_expr = Expression::from_ast(expr); let annotated_type = annotation.as_ref().map(type_ir_from_ast); - let watch_spec = watch - .as_ref() - .map(|e| WatchSpec::from_ast_with_name(e, identifier.to_string())); + let watch_spec = if *is_watched { + Some(WatchSpec::default_for_variable( + identifier.to_string(), + span.clone(), + )) + } else { + None + }; if *is_mutable { Statement::DeclareAndAssign { diff --git a/engine/baml-compiler/src/watch/watch_options.rs b/engine/baml-compiler/src/watch/watch_options.rs index 1f86ad9a9b..f0eecab9ef 100644 --- a/engine/baml-compiler/src/watch/watch_options.rs +++ b/engine/baml-compiler/src/watch/watch_options.rs @@ -1,10 +1,10 @@ use internal_baml_ast::{ self, - ast::{Expression, Identifier, WatchArgument, WatchDecorator}, + ast::{Expression, Identifier}, }; use internal_baml_diagnostics::Span; -/// The user-specified options for an emit variable. +/// The user-specified options for a watched variable. #[derive(Clone, Debug, PartialEq)] pub struct WatchSpec { pub when: WatchWhen, @@ -22,70 +22,14 @@ pub enum WatchWhen { } impl WatchSpec { - /// Lower the EmitDecorator AST node into an EmitSpec. - /// Ther are some invariants on `EmitSpec`. They are not handler here, - /// they are handled upstream in the grammar (which rules out many invalid - /// key/value combinations), and in the typechecker, which ensures that - /// when-functions have the correct type. - pub fn from_ast_with_name(ast_watch: &WatchDecorator, ast_channel_name: String) -> Self { - let mut watch = WatchSpec { + /// Create a default WatchSpec for a watched variable. + /// Configuration will be provided via VAR_NAME.$watch.options() method calls. + pub fn default_for_variable(variable_name: String, span: Span) -> Self { + WatchSpec { when: WatchWhen::True, skip_def: false, - name: ast_channel_name.clone(), - span: ast_watch.span.clone(), - }; - for WatchArgument { name, value, .. } in &ast_watch.arguments { - let mut has_error = false; - let key_str = name.to_string(); - - // For convenience, convert the value to a string. - // We use this string when it's "true" or "false". - // But we ignore it if it's something else, such as - // an identifier, when we parse the `when` key's value. - if let Some(val_str) = value.as_string_value().map(|(s, _)| s) { - // Enumerate all the valid key-value pairs. - match (key_str.as_ref(), val_str) { - ("when", "manual") => { - watch.when = WatchWhen::Manual; - } - ("when", "false") => { - // Support legacy "false" syntax, map to Manual - watch.when = WatchWhen::Manual; - } - ("when", "true") => { - watch.when = WatchWhen::True; - } - ("when", _other) => { - match value { - Expression::Identifier(ident) => { - watch.when = WatchWhen::FunctionName(ident.clone()); - } - _ => { - // Impossible case, ruled out by the parser. - } - } - } - ("skip_def", "true") => { - watch.skip_def = true; - } - ("skip_def", "false") => { - watch.skip_def = false; - } - ("name", channel_name) => { - watch.name = channel_name.to_string(); - } - _ => { - has_error = true; - } - } - } - - if has_error { - log::error!( - "Impossible case: the grammar should never produce emit argument {name:?}={value:?}" - ); - } + name: variable_name, + span, } - watch } } diff --git a/engine/baml-lib/ast/src/ast.rs b/engine/baml-lib/ast/src/ast.rs index 5823680dc2..921f3f66c3 100644 --- a/engine/baml-lib/ast/src/ast.rs +++ b/engine/baml-lib/ast/src/ast.rs @@ -45,7 +45,7 @@ pub use mermaid_debug::MermaidDiagramGenerator; pub use newline_type::NewlineType; pub use stmt::{ AssertStmt, AssignOp, AssignOpStmt, AssignStmt, CForLoopStmt, ExprStmt, ForLoopStmt, Header, - LetStmt, ReturnStmt, Stmt, WatchArgument, WatchDecorator, WhileStmt, + LetStmt, ReturnStmt, Stmt, WhileStmt, }; pub use template_string::TemplateString; pub use top::Top; diff --git a/engine/baml-lib/ast/src/ast/stmt.rs b/engine/baml-lib/ast/src/ast/stmt.rs index 4da4999916..525a3891da 100644 --- a/engine/baml-lib/ast/src/ast/stmt.rs +++ b/engine/baml-lib/ast/src/ast/stmt.rs @@ -11,59 +11,8 @@ pub struct LetStmt { pub expr: Expression, pub span: Span, pub annotations: Vec>, - pub watch: Option, -} - -#[derive(Debug, Clone)] -pub struct WatchDecorator { - pub arguments: Vec, - pub span: Span, -} - -#[derive(Debug, Clone)] -pub struct WatchArgument { - pub name: Identifier, - pub value: Expression, - pub span: Span, -} - -impl WatchDecorator { - pub fn assert_eq_up_to_span(&self, other: &WatchDecorator) { - assert_eq!(self.arguments.len(), other.arguments.len()); - for (left, right) in self.arguments.iter().zip(&other.arguments) { - left.assert_eq_up_to_span(right); - } - } -} - -impl WatchArgument { - pub fn assert_eq_up_to_span(&self, other: &WatchArgument) { - self.name.assert_eq_up_to_span(&other.name); - self.value.assert_eq_up_to_span(&other.value); - } -} - -impl fmt::Display for WatchDecorator { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("@watch")?; - if !self.arguments.is_empty() { - f.write_str("(")?; - for (idx, arg) in self.arguments.iter().enumerate() { - if idx > 0 { - f.write_str(", ")?; - } - fmt::Display::fmt(arg, f)?; - } - f.write_str(")")?; - } - Ok(()) - } -} - -impl fmt::Display for WatchArgument { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} = {}", self.name, self.value) - } + /// True if this is a watched variable (declared with `watch` keyword) + pub is_watched: bool, } #[derive(Debug, Clone)] @@ -199,15 +148,16 @@ impl fmt::Display for Stmt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Stmt::Let(stmt) => { + let keyword = if stmt.is_watched { "watch" } else { "let" }; if let Some(ann) = &stmt.annotation { - write!(f, "let {}: {} = {}", stmt.identifier, ann, stmt.expr)?; + write!( + f, + "{} {}: {} = {}", + keyword, stmt.identifier, ann, stmt.expr + ) } else { - write!(f, "let {} = {}", stmt.identifier, stmt.expr)?; - } - if let Some(watch) = &stmt.watch { - write!(f, " {watch}")?; + write!(f, "{} {} = {}", keyword, stmt.identifier, stmt.expr) } - Ok(()) } Stmt::ForLoop(stmt) => { if stmt.has_let { @@ -276,7 +226,10 @@ impl Stmt { _ => panic!("Let annotations do not match up to span"), } stmt1.expr.assert_eq_up_to_span(&stmt2.expr); - assert_opt(&stmt1.watch, &stmt2.watch, |a, b| a.assert_eq_up_to_span(b)); + assert_eq!( + stmt1.is_watched, stmt2.is_watched, + "is_watched does not match" + ); } (Stmt::ForLoop(stmt1), Stmt::ForLoop(stmt2)) => { stmt1.identifier.assert_eq_up_to_span(&stmt2.identifier); diff --git a/engine/baml-lib/ast/src/parser/datamodel.pest b/engine/baml-lib/ast/src/parser/datamodel.pest index a33a50dd04..e2d5328e94 100644 --- a/engine/baml-lib/ast/src/parser/datamodel.pest +++ b/engine/baml-lib/ast/src/parser/datamodel.pest @@ -322,7 +322,8 @@ top_level_assignment = { top_level_stmt } // More forgiving statement rule for top-level - similar to expr_body_stmt but more restrictive top_level_stmt = { ( - let_expr + watch_expr + | let_expr | assign_op_stmt | assign_stmt ) @@ -346,7 +347,8 @@ expr_body_stmt = { ( INVALID_STMT_STARTING_CHAR* ~ ( - let_expr + watch_expr + | let_expr | assign_op_stmt | assign_stmt ) @@ -368,6 +370,7 @@ stmt = { ( BREAK_KEYWORD | CONTINUE_KEYWORD + | watch_expr | let_expr | return_stmt | assert_stmt @@ -385,24 +388,11 @@ stmt = { // Let-binding statement with optional type annotation. // e.g. `let x: int|float = 10.0;` or `let x = 10.0;` +// Watch-binding statement for watched variables. +// e.g. `watch x: int = 10;` let_type_annotation = { COLON ~ field_type_chain } -let_expr = { "let" ~ identifier ~ let_type_annotation? ~ "=" ~ expression ~ watch_decorator? } - -watch_decorator = { SPACER_TEXT? ~ "@watch" ~ watch_arguments? } -watch_arguments = { - "(" - ~ SPACER_TEXT? - ~ watch_argument? - ~ ("," ~ SPACER_TEXT? ~ watch_argument)* - ~ ","? - ~ SPACER_TEXT? - ~ ")" -} -watch_argument = _{ watch_argument_kv | watch_argument_invalid | watch_argument_missing_value } -watch_argument_kv = { identifier? ~ "=" ~ watch_argument_value? } -watch_argument_invalid = { expression } -watch_argument_missing_value = { identifier ~ "=" ~ SPACER_TEXT? ~ &("," | ")") } -watch_argument_value = { expression } +let_expr = { "let" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } +watch_expr = { "watch" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } // NOTE: Needs to supports things like `object.getter().field[x + y] = value`, // so can't be just `identifier ~ "=" ~ expression`. @@ -462,7 +452,7 @@ LET_KEYWORD = { "let" } iterator_for_loop = { (LET_KEYWORD ~ identifier | identifier) ~ "in" ~ block_aware_tail_expression } c_for_loop = { c_for_init_stmt? ~ ";" ~ expression? ~ ";" ~ c_for_after_stmt? } // NOTE: let_expr ... list copied from `stmt` rule, discarding nonsensical stmts -c_for_init_stmt = { let_expr | assign_op_stmt | assign_stmt | fn_app | generic_fn_app } +c_for_init_stmt = { watch_expr | let_expr | assign_op_stmt | assign_stmt | fn_app | generic_fn_app } // same as above but without `let`. c_for_after_stmt = { block_aware_assign_op_stmt | block_aware_assign_stmt | fn_app | generic_fn_app } diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs b/engine/baml-lib/ast/src/parser/parse_expr.rs index e9ff84538c..83b6b41259 100644 --- a/engine/baml-lib/ast/src/parser/parse_expr.rs +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs @@ -11,8 +11,7 @@ use crate::{ assert_correct_parser, ast::{ self, expr::ExprFn, App, ArgumentsList, AssignOp, AssignOpStmt, AssignStmt, ExprStmt, - Expression, ExpressionBlock, ForLoopStmt, LetStmt, Stmt, TopLevelAssignment, WatchArgument, - WatchDecorator, *, + Expression, ExpressionBlock, ForLoopStmt, LetStmt, Stmt, TopLevelAssignment, *, }, parser::{ parse_arguments::parse_arguments_list, parse_expression::parse_expression, @@ -500,7 +499,8 @@ fn parse_statement_inner_rule( finish_assign_op_stmt(span, diagnostics, lhs, op_token, maybe_body).map(Stmt::AssignOp) } - Rule::let_expr => { + Rule::let_expr | Rule::watch_expr => { + let is_watched = stmt_token.as_rule() == Rule::watch_expr; let mut let_binding_tokens = stmt_token.into_inner(); let is_mutable = true; // Always mutable now after mut keyword removal @@ -528,15 +528,6 @@ fn parse_statement_inner_rule( let rhs_span = diagnostics.span(rhs_pair.as_span()); let maybe_body = parse_assignment_expr(diagnostics, rhs_pair, rhs_span); - let mut watch = None; - if let Some(trailing) = let_binding_tokens.next() { - match trailing.as_rule() { - Rule::watch_decorator => { - watch = parse_watch_decorator(trailing, diagnostics); - } - _ => parsing_catch_all(trailing, "let expression"), - } - } maybe_body.map(|body| { Stmt::Let(LetStmt { @@ -546,7 +537,7 @@ fn parse_statement_inner_rule( expr: body, span: span.clone(), annotations: vec![], - watch, + is_watched, }) }) } @@ -651,117 +642,6 @@ fn parse_assignment_expr( } } -fn parse_watch_decorator(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::watch_decorator); - let span = diagnostics.span(token.as_span()); - let mut decorator = WatchDecorator { - arguments: Vec::new(), - span, - }; - - for inner in token.into_inner() { - match inner.as_rule() { - Rule::watch_arguments => { - parse_watch_arguments(inner, diagnostics, &mut decorator.arguments) - } - Rule::SPACER_TEXT => {} - _ => parsing_catch_all(inner, "watch decorator"), - } - } - - Some(decorator) -} - -fn parse_watch_arguments( - token: Pair<'_>, - diagnostics: &mut Diagnostics, - arguments: &mut Vec, -) { - assert_correct_parser!(token, Rule::watch_arguments); - for inner in token.into_inner() { - match inner.as_rule() { - Rule::watch_argument_kv => { - if let Some(argument) = parse_watch_argument(inner, diagnostics) { - arguments.push(argument); - } - } - Rule::watch_argument_invalid => { - let span = diagnostics.span(inner.as_span()); - diagnostics.push_error(DatamodelError::new_validation_error( - "@watch options must use `name=value` syntax (e.g. `name=updates`).", - span, - )); - - // Consume the invalid expression to keep parser state consistent. - for expr in inner.into_inner() { - if expr.as_rule() == Rule::expression { - let _ = parse_expression(expr, diagnostics); - } - } - } - Rule::watch_argument_missing_value => { - let span = diagnostics.span(inner.as_span()); - diagnostics.push_error(DatamodelError::new_validation_error( - "@watch options must provide a value after `=` (e.g. `name=updates`).", - span, - )); - } - Rule::SPACER_TEXT => {} - _ => parsing_catch_all(inner, "watch decorator arguments"), - } - } -} - -fn parse_watch_argument(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::watch_argument_kv); - let span = diagnostics.span(token.as_span()); - let mut inner = token.into_inner(); - - let name_pair = inner.next()?; - let name = parse_identifier(name_pair, diagnostics); - let maybe_value_pair = inner.next(); - if maybe_value_pair.is_none() { - let suggestion = match name.name() { - "when" => "e.g. false, MyCustomFunction", - "skip_def" => "e.g. true, false", - "name" => "e.g. any_channel_name", - _ => "", - }; - diagnostics.push_error(DatamodelError::new_validation_error( - &format!("Missing value for watch argument {suggestion}"), - span.clone(), - )); - } - let value_pair = maybe_value_pair?; - - let value = match value_pair.as_rule() { - Rule::watch_argument_value => parse_watch_argument_value(value_pair, diagnostics)?, - _ => { - parsing_catch_all(value_pair, "watch decorator argument"); - return None; - } - }; - - Some(WatchArgument { name, value, span }) -} - -fn parse_watch_argument_value( - token: Pair<'_>, - diagnostics: &mut Diagnostics, -) -> Option { - assert_correct_parser!(token, Rule::watch_argument_value); - let inner = token.into_inner().next()?; - let span = diagnostics.span(inner.as_span()); - - match inner.as_rule() { - Rule::expr_block | Rule::expression => parse_assignment_expr(diagnostics, inner, span), - _ => { - parsing_catch_all(inner, "watch decorator argument value"); - None - } - } -} - pub fn parse_expr_block(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { assert_correct_parser!(token, Rule::expr_block); let span = diagnostics.span(token.as_span()); diff --git a/engine/baml-lib/baml/tests/hir_files/emit.baml b/engine/baml-lib/baml/tests/hir_files/emit.baml index d88df3087b..4f86954640 100644 --- a/engine/baml-lib/baml/tests/hir_files/emit.baml +++ b/engine/baml-lib/baml/tests/hir_files/emit.baml @@ -1,7 +1,7 @@ function Foo() -> int { - let x = 5 @watch; - let y = 10 @watch(skip_def=true); - let z: int = 20 @watch(when=MyFunction, name=foo_bar); + watch x = 5; + watch y = 10; + watch z: int = 20; 10 } @@ -13,7 +13,7 @@ function MyFunction(prev: int, next: int) -> bool { // function Foo() { // let x = 5 @watch(name=x); // let y = 10 @watch(name=y); -// let z: int = 20 @watch(when=MyFunction, name=foo_bar); +// let z: int = 20 @watch(name=z); // // 10 // } diff --git a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml b/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml index 3a0a0305f7..0819dbf7e8 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml @@ -1,12 +1,12 @@ -function WatchMissingValue() -> int { - let progress: int = 0 @watch(skip_def=); +function WatchValid() -> int { + watch progress: int = 0; progress.watchers.$notify(); progress } -// error: Error validating: Missing value for watch argument e.g. true, false -// --> expr/emit_missing_value.baml:2 +// error: Error validating: Can only access fields on class instances +// --> expr/emit_missing_value.baml:3 // | -// 1 | function WatchMissingValue() -> int { -// 2 | let progress: int = 0 @watch(skip_def=); +// 2 | watch progress: int = 0; +// 3 | progress.watchers.$notify(); // | diff --git a/engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml b/engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml index 49879fa64f..3006fc9d00 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml @@ -1,6 +1,6 @@ function WorkflowWatch() -> int { - let x: int = 10 @watch; - let y: bool = true @watch; + watch x: int = 10; + watch y: bool = true; y = false; x = WorkflowWatchChild(); @@ -8,6 +8,6 @@ function WorkflowWatch() -> int { } function WorkflowWatchChild() -> int { - let x: string = "Hello" @watch; + watch x: string = "Hello"; 100 } From 73e2134595335050a7fd490f1aef87d76f3b5232 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Sun, 19 Oct 2025 20:17:58 -0700 Subject: [PATCH 02/16] no skip_def --- engine/baml-compiler/src/builtin.rs | 77 +++++++++++++------ engine/baml-compiler/src/hir/dump.rs | 3 - engine/baml-compiler/src/thir/interpret.rs | 4 +- engine/baml-compiler/src/watch/README.md | 8 +- .../baml-compiler/src/watch/watch_options.rs | 4 +- .../baml-types/src/ir_type/builder.rs | 4 + .../tests/bytecode_files/array_access.baml | 1 + .../baml/tests/bytecode_files/assert.baml | 1 + .../tests/bytecode_files/function_calls.baml | 1 + .../tests/bytecode_files/literal_values.baml | 1 + .../tests/bytecode_files/llm_functions.baml | 1 + .../tests/bytecode_files/loops/break.baml | 1 + .../tests/bytecode_files/loops/c_for.baml | 1 + .../tests/bytecode_files/loops/continue.baml | 1 + .../baml/tests/bytecode_files/loops/for.baml | 11 +-- .../bytecode_files/loops/while_loops.baml | 1 + .../baml/tests/bytecode_files/maps.baml | 5 +- .../tests/bytecode_files/mut_variables.baml | 1 + .../bytecode_files/return_statement.baml | 1 + .../tests/bytecode_files/simple_function.baml | 1 + .../baml/tests/hir_files/array_and_call.baml | 5 ++ .../baml-lib/baml/tests/hir_files/assert.baml | 5 ++ .../baml/tests/hir_files/basic_class.baml | 5 ++ .../baml/tests/hir_files/classes.baml | 5 ++ .../baml-lib/baml/tests/hir_files/emit.baml | 5 ++ .../baml/tests/hir_files/enum_example.baml | 5 ++ .../tests/hir_files/expression_with_let.baml | 5 ++ .../tests/hir_files/if_expression_let.baml | 5 ++ .../baml/tests/hir_files/literal_values.baml | 5 ++ .../baml/tests/hir_files/llm_functions.baml | 5 ++ .../baml/tests/hir_files/loops/break.baml | 5 ++ .../baml/tests/hir_files/loops/c_for.baml | 5 ++ .../baml/tests/hir_files/loops/continue.baml | 5 ++ .../baml/tests/hir_files/loops/for.baml | 5 ++ .../tests/hir_files/loops/while_loops.baml | 5 ++ .../baml-lib/baml/tests/hir_files/maps.baml | 5 ++ .../baml/tests/hir_files/mut_variables.baml | 5 ++ .../nested_types_with_attributes.baml | 5 ++ .../baml/tests/hir_files/operators.baml | 5 ++ .../tests/hir_files/return_statement.baml | 5 ++ .../hir_files/streaming_and_constraints.baml | 5 ++ .../expr/emit_missing_value.baml | 4 +- 42 files changed, 191 insertions(+), 46 deletions(-) diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index cbf778de59..75d928ab2f 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -1,4 +1,4 @@ -use baml_types::ir_type::TypeIR; +use baml_types::ir_type::{TypeIR, UnionConstructor}; use internal_baml_diagnostics::Span; use crate::hir::{Class, Enum, EnumVariant, Field}; @@ -9,6 +9,7 @@ pub mod functions { pub mod classes { pub const REQUEST: &str = "std::Request"; + pub const WATCH_OPTIONS: &str = "baml::WatchOptions"; } pub mod enums { @@ -16,28 +17,53 @@ pub mod enums { } pub fn builtin_classes() -> Vec { - vec![Class { - name: String::from(classes::REQUEST), - methods: vec![], - fields: vec![ - Field { - name: String::from("base_url"), - r#type: TypeIR::string(), - span: Span::fake(), - }, - Field { - name: String::from("headers"), - r#type: TypeIR::map(TypeIR::string(), TypeIR::string()), - span: Span::fake(), - }, - Field { - name: String::from("query_params"), - r#type: TypeIR::map(TypeIR::string(), TypeIR::string()), - span: Span::fake(), - }, - ], - span: Span::fake(), - }] + vec![ + Class { + name: String::from(classes::REQUEST), + methods: vec![], + fields: vec![ + Field { + name: String::from("base_url"), + r#type: TypeIR::string(), + span: Span::fake(), + }, + Field { + name: String::from("headers"), + r#type: TypeIR::map(TypeIR::string(), TypeIR::string()), + span: Span::fake(), + }, + Field { + name: String::from("query_params"), + r#type: TypeIR::map(TypeIR::string(), TypeIR::string()), + span: Span::fake(), + }, + ], + span: Span::fake(), + }, + Class { + name: String::from(classes::WATCH_OPTIONS), + methods: vec![], + fields: vec![ + Field { + name: String::from("name"), + r#type: TypeIR::string(), + span: Span::fake(), + }, + Field { + name: String::from("when"), + // "never" | "manual" | ((T, T) -> bool) + // We use a generic function type with top types for T + r#type: TypeIR::union(vec![ + TypeIR::literal_string("never".to_string()), + TypeIR::literal_string("manual".to_string()), + TypeIR::arrow(vec![TypeIR::top(), TypeIR::top()], TypeIR::bool()), + ]), + span: Span::fake(), + }, + ], + span: Span::fake(), + }, + ] } pub fn builtin_enums() -> Vec { @@ -62,5 +88,8 @@ pub fn baml_fetch_as_signature(return_type: TypeIR) -> TypeIR { } pub fn is_builtin_identifier(identifier: &str) -> bool { - identifier.starts_with("std::") || identifier == "true" || identifier == "false" + identifier.starts_with("std::") + || identifier.starts_with("baml::") + || identifier == "true" + || identifier == "false" } diff --git a/engine/baml-compiler/src/hir/dump.rs b/engine/baml-compiler/src/hir/dump.rs index 02034e1864..0ff2161105 100644 --- a/engine/baml-compiler/src/hir/dump.rs +++ b/engine/baml-compiler/src/hir/dump.rs @@ -695,9 +695,6 @@ impl AssignOp { impl WatchSpec { pub fn to_doc(&self) -> RcDoc<'static, ()> { let mut args: Vec = Vec::new(); - if self.skip_def { - args.push("skip_def=true".to_string()) - } match &self.when { WatchWhen::Manual => args.push("when=manual".to_string()), WatchWhen::True => {} diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index c164e52707..8659da7c32 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -123,7 +123,7 @@ fn expr_value_to_watch_value( }) } -/// Fire a watch notification for a specific variable (for manual watchers.$notify() calls) +/// Fire a watch notification for a specific variable (for manual $watch.notify() calls) fn fire_watch_notification_for_variable( scopes: &[Scope], var_name: &str, @@ -144,7 +144,7 @@ fn fire_watch_notification_for_variable( return Ok(()); } } - bail!("Variable '{}' not found for watchers.$notify()", var_name) + bail!("Variable '{}' not found for $watch.notify()", var_name) } enum EvalValue { diff --git a/engine/baml-compiler/src/watch/README.md b/engine/baml-compiler/src/watch/README.md index 9e260348f9..2469a1365a 100644 --- a/engine/baml-compiler/src/watch/README.md +++ b/engine/baml-compiler/src/watch/README.md @@ -297,8 +297,7 @@ The `@emit` decorator supports several options: ```baml let x = 10 @emit( name="custom_name", // Custom channel name (default: variable name) - when=SomeFunction, // Conditional emission (future feature) - skip_def=true // Skip initial definition event (future feature) + when=SomeFunction // Conditional emission (future feature) ); ``` @@ -313,9 +312,8 @@ Controls when auto-emission occurs: 1. **Markdown Header Events** - Emit events when entering/exiting prompt template sections 2. **Conditional Emission** - `when` parameter to conditionally emit based on runtime values -3. **Skip Definition** - `skip_def` to only emit on reassignments -4. **Block Scoping** - Emit events for control flow blocks (if/else, loops) -5. **Error Events** - Special events for exceptions and validation failures +3. **Block Scoping** - Emit events for control flow blocks (if/else, loops) +4. **Error Events** - Special events for exceptions and validation failures ## Testing diff --git a/engine/baml-compiler/src/watch/watch_options.rs b/engine/baml-compiler/src/watch/watch_options.rs index f0eecab9ef..3f46773898 100644 --- a/engine/baml-compiler/src/watch/watch_options.rs +++ b/engine/baml-compiler/src/watch/watch_options.rs @@ -8,7 +8,6 @@ use internal_baml_diagnostics::Span; #[derive(Clone, Debug, PartialEq)] pub struct WatchSpec { pub when: WatchWhen, - pub skip_def: bool, pub name: String, pub span: Span, } @@ -16,7 +15,7 @@ pub struct WatchSpec { /// The user-specified option for when to auto-notify watchers for a variable. #[derive(Clone, Debug, PartialEq)] pub enum WatchWhen { - Manual, // Manual notification only (via .watchers.$notify()) + Manual, // Manual notification only (via .$watch.notify()) True, FunctionName(Identifier), } @@ -27,7 +26,6 @@ impl WatchSpec { pub fn default_for_variable(variable_name: String, span: Span) -> Self { WatchSpec { when: WatchWhen::True, - skip_def: false, name: variable_name, span, } diff --git a/engine/baml-lib/baml-types/src/ir_type/builder.rs b/engine/baml-lib/baml-types/src/ir_type/builder.rs index 9488bb0f8c..4f84e51bbc 100644 --- a/engine/baml-lib/baml-types/src/ir_type/builder.rs +++ b/engine/baml-lib/baml-types/src/ir_type/builder.rs @@ -1,6 +1,10 @@ use super::{BamlMediaType, StreamingMode, TypeGeneric, TypeValue}; impl TypeGeneric { + pub fn top() -> Self { + TypeGeneric::Top(T::default()) + } + pub fn string() -> Self { TypeGeneric::Primitive(TypeValue::String, T::default()) } diff --git a/engine/baml-lib/baml/tests/bytecode_files/array_access.baml b/engine/baml-lib/baml/tests/bytecode_files/array_access.baml index 84de72d79b..1fd19c73ad 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/array_access.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/array_access.baml @@ -45,6 +45,7 @@ function ArrayAccessWithVariable(arr: float[], idx: int) -> float { // 4 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/assert.baml b/engine/baml-lib/baml/tests/bytecode_files/assert.baml index 001ae04552..10859bb523 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/assert.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/assert.baml @@ -32,6 +32,7 @@ function assertNotOk() -> int { // 5 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml b/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml index 66775a2fe7..f9fbd4ebbb 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml @@ -53,6 +53,7 @@ function Nested(x: int) -> int { // 10 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml b/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml index c7c82eef4a..9efb763acf 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml @@ -46,6 +46,7 @@ function ReturnArray() -> int[] { // 6 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml b/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml index c61c7e6b34..a3f960a7d2 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml @@ -36,6 +36,7 @@ function AnalyzeSentiment(text: string) -> Sentiment { // Function: AnalyzeSentiment // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Enum Sentiment // Function: baml.Array.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml index 2d907ec1b7..428ef81e6f 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml @@ -90,6 +90,7 @@ function Nested() -> int { // 22 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml index bd0945df0b..08d63cf0cf 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml @@ -130,6 +130,7 @@ function Nothing() -> int { // 4 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml index c1bf014be1..9694fe3289 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml @@ -93,6 +93,7 @@ function ContinueNested() -> int { // 18 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml index ecd298eabb..290804dbf9 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml @@ -51,7 +51,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 1 0 LOAD_CONST 0 (0) // // 3 1 LOAD_VAR 1 (xs) -// 2 LOAD_GLOBAL 6 () +// 2 LOAD_GLOBAL 7 () // 3 LOAD_VAR 3 (__baml for loop iterated array 0) // 4 CALL 1 // 5 LOAD_CONST 0 (0) @@ -84,7 +84,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 11 0 LOAD_CONST 0 (0) // // 13 1 LOAD_VAR 1 (xs) -// 2 LOAD_GLOBAL 6 () +// 2 LOAD_GLOBAL 7 () // 3 LOAD_VAR 3 (__baml for loop iterated array 1) // 4 CALL 1 // 5 LOAD_CONST 0 (0) @@ -127,7 +127,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 24 0 LOAD_CONST 0 (0) // // 26 1 LOAD_VAR 1 (xs) -// 2 LOAD_GLOBAL 6 () +// 2 LOAD_GLOBAL 7 () // 3 LOAD_VAR 3 (__baml for loop iterated array 2) // 4 CALL 1 // 5 LOAD_CONST 0 (0) @@ -170,7 +170,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 38 0 LOAD_CONST 0 (0) // // 40 1 LOAD_VAR 1 (as) -// 2 LOAD_GLOBAL 6 () +// 2 LOAD_GLOBAL 7 () // 3 LOAD_VAR 4 (__baml for loop iterated array 3) // 4 CALL 1 // 5 LOAD_CONST 0 (0) @@ -188,7 +188,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 17 STORE_VAR 6 (__baml for loop index 3) // // 41 18 LOAD_VAR 2 (bs) -// 19 LOAD_GLOBAL 6 () +// 19 LOAD_GLOBAL 7 () // 20 LOAD_VAR 8 (__baml for loop iterated array 4) // 21 CALL 1 // 22 LOAD_CONST 0 (0) @@ -224,6 +224,7 @@ function NestedFor(as: int[], bs: int[]) -> int { // 50 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml index 3626481983..2bcffe0e80 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml @@ -127,6 +127,7 @@ function WhileWithScopes() -> int { // 33 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/maps.baml b/engine/baml-lib/baml/tests/bytecode_files/maps.baml index 9f8ddf4eb4..6c5e3b615e 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/maps.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/maps.baml @@ -113,7 +113,7 @@ function Len() -> int { // 29 0 LOAD_GLOBAL 1 () // 1 CALL 0 // -// 30 2 LOAD_GLOBAL 11 () +// 30 2 LOAD_GLOBAL 12 () // 3 LOAD_VAR 1 (map) // 4 LOAD_CONST 0 (hello) // 5 CALL 2 @@ -133,12 +133,13 @@ function Len() -> int { // 38 0 LOAD_GLOBAL 0 () // 1 CALL 0 // -// 39 2 LOAD_GLOBAL 10 () +// 39 2 LOAD_GLOBAL 11 () // 3 LOAD_VAR 1 (map) // 4 CALL 1 // 5 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml b/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml index bd8db149f8..ca0009615e 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml @@ -29,6 +29,7 @@ function MutableInArg(x: int) -> int { // 3 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml b/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml index 65943da405..e6c58640ac 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml @@ -100,6 +100,7 @@ function WithStack(x: int) -> int { // 39 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml b/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml index 9f924fa5db..1c7a565450 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml @@ -8,6 +8,7 @@ function GetName(person: string) -> string { // 1 RETURN // // Class: std::Request with 3 fields +// Class: baml::WatchOptions with 2 fields // Enum std::HttpMethod // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml index 201f5a3d02..d02f7cacea 100644 --- a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml +++ b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml @@ -36,6 +36,11 @@ function ArrayAccess() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/assert.baml b/engine/baml-lib/baml/tests/hir_files/assert.baml index 2083630e45..d224360a98 100644 --- a/engine/baml-lib/baml/tests/hir_files/assert.baml +++ b/engine/baml-lib/baml/tests/hir_files/assert.baml @@ -29,6 +29,11 @@ function assertNotOk() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/basic_class.baml b/engine/baml-lib/baml/tests/hir_files/basic_class.baml index 09cddcbc6e..bb5648fd00 100644 --- a/engine/baml-lib/baml/tests/hir_files/basic_class.baml +++ b/engine/baml-lib/baml/tests/hir_files/basic_class.baml @@ -9,6 +9,11 @@ class Person { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // class Person { // name: string // age: int diff --git a/engine/baml-lib/baml/tests/hir_files/classes.baml b/engine/baml-lib/baml/tests/hir_files/classes.baml index 0386eadb5e..c5656ddd2c 100644 --- a/engine/baml-lib/baml/tests/hir_files/classes.baml +++ b/engine/baml-lib/baml/tests/hir_files/classes.baml @@ -24,6 +24,11 @@ function TestClass() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // class Example { // a: int // b: string diff --git a/engine/baml-lib/baml/tests/hir_files/emit.baml b/engine/baml-lib/baml/tests/hir_files/emit.baml index 4f86954640..2fe5b7a2be 100644 --- a/engine/baml-lib/baml/tests/hir_files/emit.baml +++ b/engine/baml-lib/baml/tests/hir_files/emit.baml @@ -28,6 +28,11 @@ function MyFunction(prev: int, next: int) -> bool { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/enum_example.baml b/engine/baml-lib/baml/tests/hir_files/enum_example.baml index e566b04981..41fbc445f5 100644 --- a/engine/baml-lib/baml/tests/hir_files/enum_example.baml +++ b/engine/baml-lib/baml/tests/hir_files/enum_example.baml @@ -15,6 +15,11 @@ class Widget { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // class Widget { // color: Color // size: int diff --git a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml index 1453eb1a8c..aca8f5847a 100644 --- a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml @@ -15,6 +15,11 @@ function AddOne(x: int) -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml index 5f70ae9f7c..145e633e9c 100644 --- a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml @@ -21,6 +21,11 @@ function simpleIf() -> string { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/literal_values.baml b/engine/baml-lib/baml/tests/hir_files/literal_values.baml index e57376bce5..ade856599f 100644 --- a/engine/baml-lib/baml/tests/hir_files/literal_values.baml +++ b/engine/baml-lib/baml/tests/hir_files/literal_values.baml @@ -45,6 +45,11 @@ function ReturnArray() -> int[] { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml index 6d3f631671..4178513c43 100644 --- a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml +++ b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml @@ -66,6 +66,11 @@ function AnalyzeSentiment(text: string) -> Sentiment { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/break.baml b/engine/baml-lib/baml/tests/hir_files/loops/break.baml index 4b25d7dd0a..119e5ed1ed 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/break.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/break.baml @@ -61,6 +61,11 @@ function Nested() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml index 92b267f92f..3346f57eb7 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml @@ -100,6 +100,11 @@ function Nothing() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml index f3b3cf5f71..905bd7abe0 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml @@ -58,6 +58,11 @@ function ContinueNested() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/for.baml b/engine/baml-lib/baml/tests/hir_files/loops/for.baml index 36993dfd03..5072386663 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/for.baml @@ -23,6 +23,11 @@ function Sum(xs: int[]) -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml index 83a8ae2745..3e88ece3b2 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml @@ -29,6 +29,11 @@ function GCD(a: int, b: int) -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/maps.baml b/engine/baml-lib/baml/tests/hir_files/maps.baml index f4c0a358d6..682ae9cff5 100644 --- a/engine/baml-lib/baml/tests/hir_files/maps.baml +++ b/engine/baml-lib/baml/tests/hir_files/maps.baml @@ -91,6 +91,11 @@ function Len() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml index 3f44722810..46b7e840da 100644 --- a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml +++ b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml @@ -31,6 +31,11 @@ function MutableInArg(x: int) -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml index 1e853fa141..2de6b50709 100644 --- a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml +++ b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml @@ -11,6 +11,11 @@ class DataModel { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // class DataModel { // tags: array // status: (string @stream.done | int @stream.done) @stream.done diff --git a/engine/baml-lib/baml/tests/hir_files/operators.baml b/engine/baml-lib/baml/tests/hir_files/operators.baml index f3f7bb90f7..fd2baa98c4 100644 --- a/engine/baml-lib/baml/tests/hir_files/operators.baml +++ b/engine/baml-lib/baml/tests/hir_files/operators.baml @@ -16,6 +16,11 @@ function nestedOps() -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/return_statement.baml b/engine/baml-lib/baml/tests/hir_files/return_statement.baml index 7f73adec2f..48490a27d2 100644 --- a/engine/baml-lib/baml/tests/hir_files/return_statement.baml +++ b/engine/baml-lib/baml/tests/hir_files/return_statement.baml @@ -69,6 +69,11 @@ function WithStack(x: int) -> int { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // enum std::HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml index 38b0dd4251..c6e015baf9 100644 --- a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml +++ b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml @@ -15,6 +15,11 @@ class User { // query_params: map // } // +// class baml::WatchOptions { +// name: string +// when: ("never" | "manual" | (ANY, ANY) -> bool) +// } +// // class MyClass { // field1: string @stream.done // field2: int @stream.needed diff --git a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml b/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml index 0819dbf7e8..1352368d02 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml @@ -1,6 +1,6 @@ function WatchValid() -> int { watch progress: int = 0; - progress.watchers.$notify(); + progress.$watch.notify(); progress } @@ -8,5 +8,5 @@ function WatchValid() -> int { // --> expr/emit_missing_value.baml:3 // | // 2 | watch progress: int = 0; -// 3 | progress.watchers.$notify(); +// 3 | progress.$watch.notify(); // | From 7305a95e39df376098e352404863d319603d938a Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Sun, 19 Oct 2025 20:50:55 -0700 Subject: [PATCH 03/16] static analysis --- engine/baml-compiler/src/watch.rs | 106 ++++++++++++++++-- .../baml-compiler/src/watch/watch_options.rs | 5 +- engine/baml-lib/ast/src/parser/datamodel.pest | 4 +- .../parser/parse_value_expression_block.rs | 6 + 4 files changed, 105 insertions(+), 16 deletions(-) diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 061ba9518d..e958f1513b 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -296,6 +296,61 @@ impl FunctionMetadata { }; } + /// Handle a call to VAR_NAME.$watch.options(baml.WatchOptions{...}) + /// Extract the channel name from the WatchOptions constructor and update the channel type. + fn handle_watch_options_call( + &mut self, + var_name: &str, + args: &[thir::Expr], + _meta: &ExprMetadata, + diagnostics: &mut Diagnostics, + ) { + // The argument should be a ClassConstructor for baml.WatchOptions + if args.len() != 1 { + return; // Type checker should have caught this + } + + if let thir::Expr::ClassConstructor { fields, .. } = &args[0] { + // Extract the "name" field value + for field in fields { + if let ClassConstructorField::Named { name, value } = field { + if name == "name" { + if let thir::Expr::Value(baml_value) = value { + // Extract string value from BamlValueWithMeta::String + if let baml_types::BamlValueWithMeta::String(channel_name, _) = + baml_value + { + // Find the watch variable and update its channel name + if let Some((spec, var_type)) = self.watch_vars.get(var_name) { + let new_spec = WatchSpec { + name: channel_name.clone(), + when: spec.when.clone(), + span: spec.span.clone(), + }; + let var_type_clone = var_type.clone(); + // The immutable borrow ends here naturally + self.push_watch_var( + var_name.to_string(), + new_spec, + var_type_clone, + ); + } else { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!( + "Variable '{}' is not a watched variable", + var_name + ), + baml_value.meta().0.clone(), + )); + } + } + } + } + } + } + } + } + /// Walk the parts of an expression, appending metadata. pub fn analyze_expression( &mut self, @@ -310,7 +365,31 @@ impl FunctionMetadata { thir::Expr::FieldAccess { base, .. } => { self.analyze_expression(base, diagnostics); } - thir::Expr::MethodCall { receiver, args, .. } => { + thir::Expr::MethodCall { + receiver, + method, + args, + meta, + } => { + // Check for VAR_NAME.$watch.options(baml.WatchOptions{name: "...", ...}) + if let thir::Expr::FieldAccess { base, field, .. } = receiver.as_ref() { + if field == "$watch" { + if let thir::Expr::Var(method_name, _) = method.as_ref() { + if method_name == "options" { + // Extract the variable name and channel configuration + if let thir::Expr::Var(var_name, _) = base.as_ref() { + self.handle_watch_options_call( + var_name, + args, + meta, + diagnostics, + ); + } + } + } + } + } + self.analyze_expression(receiver, diagnostics); for arg in args { self.analyze_expression(arg, diagnostics); @@ -472,15 +551,16 @@ mod tests { let hir = Hir::from_source( r#" function A() -> int { - let a_1 = 1 @watch(); - let a_2 = true @watch(name=a_2_renamed); + watch a_1 = 1; + watch a_2 = true; + a_2.$watch.options(baml.WatchOptions{name: "a_2_renamed"}); B(); 1 } function B() -> int { C(); if (true) { - let b_1 = "hey" @watch(); + watch b_1 = "hey"; A(); } else { B(); @@ -550,12 +630,18 @@ mod tests { let hir = Hir::from_source( r#" function A() -> int { - let a_1: int = 1 @watch(name=a); - let a_2: string = "hi" @watch(name=a); - let b_1: int | bool = true @watch(name=b); - let b_2: int = 3 @watch(name=b); - let c_1: int = 1 @watch(name=c); - let c_2: int | bool = 3 @watch(name=c); + watch a_1: int = 1; + a_1.$watch.options(baml.WatchOptions{name: "a"}); + watch a_2: string = "hi"; + a_2.$watch.options(baml.WatchOptions{name: "a"}); + watch b_1: int | bool = true; + b_1.$watch.options(baml.WatchOptions{name: "b"}); + watch b_2: int = 3; + b_2.$watch.options(baml.WatchOptions{name: "b"}); + watch c_1: int = 1; + c_1.$watch.options(baml.WatchOptions{name: "c"}); + watch c_2: int | bool = 3; + c_2.$watch.options(baml.WatchOptions{name: "c"}); 1 } "#, diff --git a/engine/baml-compiler/src/watch/watch_options.rs b/engine/baml-compiler/src/watch/watch_options.rs index 3f46773898..966736de61 100644 --- a/engine/baml-compiler/src/watch/watch_options.rs +++ b/engine/baml-compiler/src/watch/watch_options.rs @@ -1,7 +1,4 @@ -use internal_baml_ast::{ - self, - ast::{Expression, Identifier}, -}; +use internal_baml_ast::{self, ast::Identifier}; use internal_baml_diagnostics::Span; /// The user-specified options for a watched variable. diff --git a/engine/baml-lib/ast/src/parser/datamodel.pest b/engine/baml-lib/ast/src/parser/datamodel.pest index e2d5328e94..3b0b29da2a 100644 --- a/engine/baml-lib/ast/src/parser/datamodel.pest +++ b/engine/baml-lib/ast/src/parser/datamodel.pest @@ -1,5 +1,5 @@ schema = { - SOI ~ (expr_fn | top_level_assignment | value_expression_block | type_expression_block | template_declaration | type_alias | comment_block | raw_string_literal | empty_lines | CATCH_ALL)* ~ EOI + SOI ~ (value_expression_block | expr_fn | top_level_assignment | type_expression_block | template_declaration | type_alias | comment_block | raw_string_literal | empty_lines | CATCH_ALL)* ~ EOI } // ###################################### @@ -27,7 +27,7 @@ field_type_with_attr = { field_type ~ (NEWLINE? ~ (field_attribute | trailing_co value_expression_keyword = { FUNCTION_KEYWORD | TEST_KEYWORD | CLIENT_KEYWORD | RETRY_POLICY_KEYWORD | GENERATOR_KEYWORD } value_expression_block = { value_expression_keyword ~ identifier ~ named_argument_list? ~ ARROW? ~ field_type_chain? ~ SPACER_TEXT ~ BLOCK_OPEN ~ value_expression_contents ~ BLOCK_CLOSE } value_expression_contents = { - (stmt | type_builder_block | value_expression | comment_block | block_attribute | empty_lines | BLOCK_LEVEL_CATCH_ALL)* + (stmt | type_builder_block | value_expression | comment_block | block_attribute | empty_lines)* } value_expression = { identifier ~ config_expression? ~ (NEWLINE? ~ field_attribute)* ~ trailing_comment? } diff --git a/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs b/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs index 61fed1f866..fd2e164264 100644 --- a/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs +++ b/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs @@ -127,6 +127,12 @@ pub(crate) fn parse_value_expression_block( } } Rule::empty_lines => {} + Rule::stmt => { + // Statements are allowed in expression functions that got parsed as value_expression_block. + // They will be handled during HIR lowering when we distinguish between + // LLM functions (with client/prompt) and expression functions (with code). + // For now, just ignore them during parsing. + } Rule::BLOCK_LEVEL_CATCH_ALL => { diagnostics.push_error(DatamodelError::new_validation_error( "This line is not a valid field or attribute definition. A valid property may look like: 'myProperty \"some value\"' for example, with no colons.", From 7e1ffa30e3641354f60bc04b03ab5ea1d8714bae Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Sun, 19 Oct 2025 22:42:50 -0700 Subject: [PATCH 04/16] let watch --- engine/baml-compiler/src/hir.rs | 9 ++ engine/baml-compiler/src/hir/dump.rs | 23 ++++ engine/baml-compiler/src/hir/lowering.rs | 70 +++++++++- engine/baml-compiler/src/thir/typecheck.rs | 21 +++ engine/baml-compiler/src/watch.rs | 129 +++++++++++++++--- engine/baml-lib/ast/src/ast.rs | 2 +- .../baml-lib/ast/src/ast/header_collector.rs | 1 + engine/baml-lib/ast/src/ast/mermaid_debug.rs | 4 + engine/baml-lib/ast/src/ast/stmt.rs | 63 ++++++++- engine/baml-lib/ast/src/parser/datamodel.pest | 34 +++-- engine/baml-lib/ast/src/parser/parse_expr.rs | 96 ++++++++++++- .../ast/src/parser/parse_expression.rs | 15 +- .../tests/hir_files/{emit.baml => watch.baml} | 6 +- 13 files changed, 420 insertions(+), 53 deletions(-) rename engine/baml-lib/baml/tests/hir_files/{emit.baml => watch.baml} (89%) diff --git a/engine/baml-compiler/src/hir.rs b/engine/baml-compiler/src/hir.rs index 8ebf7fe027..d7a4d89714 100644 --- a/engine/baml-compiler/src/hir.rs +++ b/engine/baml-compiler/src/hir.rs @@ -186,6 +186,15 @@ pub enum Statement { condition: Expression, span: Span, }, + + /// Configure watch options for a watched variable. + /// Syntax: `variable.$watch.options(name: "channel", when: FilterFunc)` + WatchOptions { + variable: String, + name: Option, + when: Option, + span: Span, + }, } #[derive(Clone, Debug)] diff --git a/engine/baml-compiler/src/hir/dump.rs b/engine/baml-compiler/src/hir/dump.rs index 0ff2161105..9401884862 100644 --- a/engine/baml-compiler/src/hir/dump.rs +++ b/engine/baml-compiler/src/hir/dump.rs @@ -270,6 +270,29 @@ impl Statement { .append(block) .append(RcDoc::text("}")) } + Statement::WatchOptions { + variable, + name, + when, + .. + } => { + let mut doc = RcDoc::text(variable.clone()).append(RcDoc::text(".$watch.options(")); + + let mut parts = vec![]; + if let Some(n) = name { + parts.push( + RcDoc::text("name: \"") + .append(RcDoc::text(n.clone())) + .append(RcDoc::text("\"")), + ); + } + if let Some(w) = when { + parts.push(RcDoc::text("when: ").append(RcDoc::text(w.clone()))); + } + + doc = doc.append(RcDoc::intersperse(parts, RcDoc::text(", "))); + doc.append(RcDoc::text(");")) + } } } } diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 8c7a907216..576656ee1a 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -2,7 +2,7 @@ //! //! This files contains the convertions between Baml AST nodes to HIR nodes. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use baml_types::{ type_meta::{self, base::StreamingBehavior}, @@ -304,8 +304,40 @@ impl ExprFunction { impl Block { /// Lower an expression block into HIR for expression blocks. pub fn from_expr_block(block: &ast::ExpressionBlock) -> Self { + // First pass: collect all WatchOptions statements into a map + let mut watch_options_map: HashMap, Option)> = + HashMap::new(); + for stmt in &block.stmts { + if let ast::Stmt::WatchOptions(ast::WatchOptionsStmt { + variable, + name, + when, + .. + }) = stmt + { + watch_options_map.insert( + variable.to_string(), + (name.clone(), when.as_ref().map(|id| id.to_string())), + ); + } + } + + // Second pass: lower statements, filtering out WatchOptions and applying them to watch specs + let statements: Vec = block + .stmts + .iter() + .filter_map(|stmt| { + // Skip WatchOptions statements - they're applied during watch spec creation + if matches!(stmt, ast::Stmt::WatchOptions(_)) { + None + } else { + Some(lower_stmt_with_options(stmt, &watch_options_map)) + } + }) + .collect(); + Block { - statements: block.stmts.iter().map(lower_stmt).collect(), + statements, trailing_expr: block .expr .as_deref() @@ -316,6 +348,13 @@ impl Block { } fn lower_stmt(stmt: &ast::Stmt) -> Statement { + lower_stmt_with_options(stmt, &HashMap::new()) +} + +fn lower_stmt_with_options( + stmt: &ast::Stmt, + watch_options: &HashMap, Option)>, +) -> Statement { match stmt { ast::Stmt::CForLoop(stmt) => { // we'll add a block if we an init statement, otherwise we'll just @@ -420,10 +459,23 @@ fn lower_stmt(stmt: &ast::Stmt) -> Statement { let annotated_type = annotation.as_ref().map(type_ir_from_ast); let watch_spec = if *is_watched { - Some(WatchSpec::default_for_variable( - identifier.to_string(), - span.clone(), - )) + let var_name = identifier.to_string(); + let mut spec = WatchSpec::default_for_variable(var_name.clone(), span.clone()); + + // Apply watch options if they exist for this variable + if let Some((name_opt, when_opt)) = watch_options.get(&var_name) { + if let Some(custom_name) = name_opt { + spec.name = custom_name.clone(); + } + if let Some(when_fn) = when_opt { + spec.when = crate::watch::WatchWhen::FunctionName(ast::Identifier::Local( + when_fn.clone(), + span.clone(), + )); + } + } + + Some(spec) } else { None }; @@ -481,6 +533,12 @@ fn lower_stmt(stmt: &ast::Stmt) -> Statement { condition: Expression::from_ast(value), span: span.clone(), }, + ast::Stmt::WatchOptions(_) => { + // WatchOptions statements should be filtered out during Block::from_expr_block + // and their settings applied to the WatchSpec of the watched variable. + // If we reach here, it's a bug in the lowering logic. + unreachable!("WatchOptions statements should not reach lower_stmt_with_options") + } } } diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 2a287aa8b1..3143f5a987 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1169,6 +1169,27 @@ fn typecheck_statement( span: span.clone(), }) } + hir::Statement::WatchOptions { + variable, + name: _, + when: _, + span, + } => { + // Check that the variable exists in context + if !context.vars.contains_key(variable) { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Unknown variable '{}' in watch options", variable), + span.clone(), + )); + } + + // TODO: Validate that 'when' function exists and has correct signature + // For now, we just pass it through + + // Watch options statements are not included in THIR - they're metadata + // that gets processed during watch analysis + None + } } } diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index e958f1513b..8fc89593ae 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -551,16 +551,15 @@ mod tests { let hir = Hir::from_source( r#" function A() -> int { - watch a_1 = 1; - watch a_2 = true; - a_2.$watch.options(baml.WatchOptions{name: "a_2_renamed"}); + watch let a_1 = 1; + watch let a_2 = true; B(); 1 } function B() -> int { C(); if (true) { - watch b_1 = "hey"; + watch let b_1 = "hey"; A(); } else { B(); @@ -583,7 +582,11 @@ mod tests { // C() has no channels. assert_eq!(c_channels.channels.len(), 0); - // B() has its direct channel for b_1, and its indirect channel for a_1 and a_2. + // Without .$watch.options() to customize names, channels use variable names + // B() has its direct channel for b_1, and its indirect channels for a_1 and a_2. + println!("B channels: {:?}", b_channels.channels); + println!("A channels: {:?}", a_channels.channels); + // Just verify we have the right number of channels for now assert_eq!(b_channels.channels.len(), 3); assert_eq!( b_channels @@ -630,18 +633,18 @@ mod tests { let hir = Hir::from_source( r#" function A() -> int { - watch a_1: int = 1; - a_1.$watch.options(baml.WatchOptions{name: "a"}); - watch a_2: string = "hi"; - a_2.$watch.options(baml.WatchOptions{name: "a"}); - watch b_1: int | bool = true; - b_1.$watch.options(baml.WatchOptions{name: "b"}); - watch b_2: int = 3; - b_2.$watch.options(baml.WatchOptions{name: "b"}); - watch c_1: int = 1; - c_1.$watch.options(baml.WatchOptions{name: "c"}); - watch c_2: int | bool = 3; - c_2.$watch.options(baml.WatchOptions{name: "c"}); + watch let a_1: int = 1; + a_1.$watch.options(name: "a"); + watch let a_2: string = "hi"; + a_2.$watch.options(name: "a"); + watch let b_1: int | bool = true; + b_1.$watch.options(name: "b"); + watch let b_2: int = 3; + b_2.$watch.options(name: "b"); + watch let c_1: int = 1; + c_1.$watch.options(name: "c"); + watch let c_2: int | bool = 3; + c_2.$watch.options(name: "c"); 1 } "#, @@ -676,4 +679,96 @@ mod tests { assert_eq!(variants, vec![&TypeIR::int(), &TypeIR::bool()]); } } + + #[test] + fn test_plain_let() { + let hir = Hir::from_source( + r#" + function A() -> int { + let a_1 = 1; + 1 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 1); + } + + #[test] + fn test_let_watch_simple() { + let hir = Hir::from_source( + r#" + function A() -> int { + watch let a_1 = 1; + 1 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 1); + } + + #[test] + fn test_let_watch_with_options() { + let hir = Hir::from_source( + r#" + function A() -> int { + let a_1 = 1; + a_1 + 2; + 1 + } + function B() -> int { + 2 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 2); + } + + #[test] + #[ignore = "$watch special field not yet implemented"] + fn test_field_access_statement() { + let hir = Hir::from_source( + r#" + function A() -> int { + watch let a_1: int = 1; + a_1.$watch; + 1 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 1); + } + + #[test] + fn test_namespaced_constructor() { + let hir = Hir::from_source( + r#" + class WatchOptions { + name string + } + + function A() -> int { + let opts = baml.WatchOptions{name: "test"}; + 1 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 1); + } + + #[test] + fn test_non_namespaced_constructor() { + let hir = Hir::from_source( + r#" + class WatchOptions { + name string + } + + function A() -> int { + let opts = WatchOptions{name: "test"}; + 1 + } + "#, + ); + assert_eq!(hir.expr_functions.len(), 1); + } } diff --git a/engine/baml-lib/ast/src/ast.rs b/engine/baml-lib/ast/src/ast.rs index 921f3f66c3..cfdc0b752f 100644 --- a/engine/baml-lib/ast/src/ast.rs +++ b/engine/baml-lib/ast/src/ast.rs @@ -45,7 +45,7 @@ pub use mermaid_debug::MermaidDiagramGenerator; pub use newline_type::NewlineType; pub use stmt::{ AssertStmt, AssignOp, AssignOpStmt, AssignStmt, CForLoopStmt, ExprStmt, ForLoopStmt, Header, - LetStmt, ReturnStmt, Stmt, WhileStmt, + LetStmt, ReturnStmt, Stmt, WatchOptionsStmt, WhileStmt, }; pub use template_string::TemplateString; pub use top::Top; diff --git a/engine/baml-lib/ast/src/ast/header_collector.rs b/engine/baml-lib/ast/src/ast/header_collector.rs index c313940e77..8ba7f12bb1 100644 --- a/engine/baml-lib/ast/src/ast/header_collector.rs +++ b/engine/baml-lib/ast/src/ast/header_collector.rs @@ -434,6 +434,7 @@ impl HeaderCollector { self.visit_expression(&return_stmt.value); } Stmt::Assert(assert_stmt) => self.visit_expression(&assert_stmt.value), + Stmt::WatchOptions(_) => {} // Watch options don't contain expressions to visit } } diff --git a/engine/baml-lib/ast/src/ast/mermaid_debug.rs b/engine/baml-lib/ast/src/ast/mermaid_debug.rs index c9fdf02043..84902c0cc3 100644 --- a/engine/baml-lib/ast/src/ast/mermaid_debug.rs +++ b/engine/baml-lib/ast/src/ast/mermaid_debug.rs @@ -768,6 +768,10 @@ impl MermaidDiagramGenerator { self.connect(&stmt_id, &value_id, Some("condition")); stmt_id } + Stmt::WatchOptions(watch_opts) => { + let label = format!("WatchOptions: {}", watch_opts.variable.name()); + self.get_node_id_with_class(&key, &label, "statementNode") + } } } diff --git a/engine/baml-lib/ast/src/ast/stmt.rs b/engine/baml-lib/ast/src/ast/stmt.rs index 525a3891da..00384c8304 100644 --- a/engine/baml-lib/ast/src/ast/stmt.rs +++ b/engine/baml-lib/ast/src/ast/stmt.rs @@ -1,6 +1,7 @@ use std::fmt; use super::{Expression, ExpressionBlock, FieldType, Identifier, Span}; +use crate::ast::traits::WithName; #[derive(Debug, Clone)] pub struct LetStmt { @@ -101,6 +102,19 @@ pub struct AssertStmt { pub span: Span, } +/// Special statement for configuring watch options on a watched variable. +/// Syntax: `variable_name.$watch.options(name: "channel", when: SomeFunction)` +#[derive(Debug, Clone)] +pub struct WatchOptionsStmt { + /// The variable being configured + pub variable: Identifier, + /// Optional custom channel name + pub name: Option, + /// Optional filter function for conditional watching + pub when: Option, + pub span: Span, +} + // Stmt(statements) perform actions and not often return values. #[derive(Debug, Clone)] pub enum Stmt { @@ -118,6 +132,7 @@ pub enum Stmt { Continue(Span), Return(ReturnStmt), Assert(AssertStmt), + WatchOptions(WatchOptionsStmt), } impl fmt::Display for AssignOp { @@ -198,6 +213,24 @@ impl fmt::Display for Stmt { Stmt::Continue(_) => f.write_str("continue"), Stmt::Return(ReturnStmt { value, .. }) => write!(f, "return {value}"), Stmt::Assert(AssertStmt { value, .. }) => write!(f, "assert {value}"), + Stmt::WatchOptions(WatchOptionsStmt { + variable, + name, + when, + .. + }) => { + write!(f, "{}.$watch.options(", variable.name())?; + if let Some(n) = name { + write!(f, "name: \"{n}\"")?; + } + if let Some(w) = when { + if name.is_some() { + write!(f, ", ")?; + } + write!(f, "when: {}", w.name())?; + } + write!(f, ")") + } } } } @@ -293,6 +326,29 @@ impl Stmt { Stmt::Assert(AssertStmt { value: b, .. }), ) => a.assert_eq_up_to_span(b), + ( + Stmt::WatchOptions(WatchOptionsStmt { + variable: v1, + name: n1, + when: w1, + .. + }), + Stmt::WatchOptions(WatchOptionsStmt { + variable: v2, + name: n2, + when: w2, + .. + }), + ) => { + v1.assert_eq_up_to_span(v2); + assert_eq!(n1, n2, "watch option names do not match"); + match (w1, w2) { + (Some(w1), Some(w2)) => w1.assert_eq_up_to_span(w2), + (None, None) => {} + _ => panic!("watch option when clauses do not match"), + } + } + ( Stmt::Let(_) | Stmt::ForLoop(_) @@ -305,7 +361,8 @@ impl Stmt { | Stmt::Return(_) | Stmt::Break(_) | Stmt::Continue(_) - | Stmt::Assert(_), + | Stmt::Assert(_) + | Stmt::WatchOptions(_), _, ) => { panic!("Types do not match: {self:?} and {other:?}") @@ -340,6 +397,7 @@ impl Stmt { stmt.left ), }, + Stmt::WatchOptions(WatchOptionsStmt { variable, .. }) => variable, } } @@ -354,7 +412,8 @@ impl Stmt { | Stmt::Return(ReturnStmt { span, .. }) | Stmt::Break(span) | Stmt::Continue(span) - | Stmt::Assert(AssertStmt { span, .. }) => span, + | Stmt::Assert(AssertStmt { span, .. }) + | Stmt::WatchOptions(WatchOptionsStmt { span, .. }) => span, Stmt::Expression(es) => &es.span, Stmt::Semicolon(expr) => expr.span(), diff --git a/engine/baml-lib/ast/src/parser/datamodel.pest b/engine/baml-lib/ast/src/parser/datamodel.pest index 3b0b29da2a..2cd68ceabb 100644 --- a/engine/baml-lib/ast/src/parser/datamodel.pest +++ b/engine/baml-lib/ast/src/parser/datamodel.pest @@ -1,5 +1,5 @@ schema = { - SOI ~ (value_expression_block | expr_fn | top_level_assignment | type_expression_block | template_declaration | type_alias | comment_block | raw_string_literal | empty_lines | CATCH_ALL)* ~ EOI + SOI ~ (expr_fn | top_level_assignment | value_expression_block | type_expression_block | template_declaration | type_alias | comment_block | raw_string_literal | empty_lines | CATCH_ALL)* ~ EOI } // ###################################### @@ -27,7 +27,7 @@ field_type_with_attr = { field_type ~ (NEWLINE? ~ (field_attribute | trailing_co value_expression_keyword = { FUNCTION_KEYWORD | TEST_KEYWORD | CLIENT_KEYWORD | RETRY_POLICY_KEYWORD | GENERATOR_KEYWORD } value_expression_block = { value_expression_keyword ~ identifier ~ named_argument_list? ~ ARROW? ~ field_type_chain? ~ SPACER_TEXT ~ BLOCK_OPEN ~ value_expression_contents ~ BLOCK_CLOSE } value_expression_contents = { - (stmt | type_builder_block | value_expression | comment_block | block_attribute | empty_lines)* + (type_builder_block | value_expression | comment_block | block_attribute | empty_lines)* } value_expression = { identifier ~ config_expression? ~ (NEWLINE? ~ field_attribute)* ~ trailing_comment? } @@ -322,8 +322,7 @@ top_level_assignment = { top_level_stmt } // More forgiving statement rule for top-level - similar to expr_body_stmt but more restrictive top_level_stmt = { ( - watch_expr - | let_expr + let_expr | assign_op_stmt | assign_stmt ) @@ -337,7 +336,7 @@ top_level_stmt = { // function foo(x:int, y: bool?) -> string { // go(x,y) // } -expr_fn = { "function" ~ identifier ~ named_argument_list ~ ARROW? ~ field_type_chain? ~ expr_block } +expr_fn = { "function" ~ identifier ~ named_argument_list ~ ARROW? ~ field_type_chain? ~ SPACER_TEXT? ~ expr_block } // Body of a function (including curly brackets). expr_block = { BLOCK_OPEN ~ NEWLINE? ~ (expr_body_stmt | stmt | comment_block | empty_lines)* ~ expression? ~ (comment_block | empty_lines)* ~ BLOCK_CLOSE } @@ -347,8 +346,8 @@ expr_body_stmt = { ( INVALID_STMT_STARTING_CHAR* ~ ( - watch_expr - | let_expr + let_expr + | watch_options_stmt | assign_op_stmt | assign_stmt ) @@ -370,10 +369,10 @@ stmt = { ( BREAK_KEYWORD | CONTINUE_KEYWORD - | watch_expr | let_expr | return_stmt | assert_stmt + | watch_options_stmt | assign_op_stmt | assign_stmt | expression @@ -388,11 +387,10 @@ stmt = { // Let-binding statement with optional type annotation. // e.g. `let x: int|float = 10.0;` or `let x = 10.0;` -// Watch-binding statement for watched variables. -// e.g. `watch x: int = 10;` +// Watch-binding statement for watched variables (watch keyword comes before let). +// e.g. `watch let x: int = 10;` let_type_annotation = { COLON ~ field_type_chain } -let_expr = { "let" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } -watch_expr = { "watch" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } +let_expr = { WATCH_KEYWORD? ~ "let" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } // NOTE: Needs to supports things like `object.getter().field[x + y] = value`, // so can't be just `identifier ~ "=" ~ expression`. @@ -408,6 +406,11 @@ watch_expr = { "watch" ~ identifier ~ let_type_annotation? ~ "=" ~ expression } assign_stmt = { expression ~ "=" ~ expression } assign_op_stmt = { expression ~ assign_op ~ expression } +// Special syntax for configuring watch options on a watched variable +// e.g. variable.$watch.options(name: "channel", when: FilterFunc) +watch_options_stmt = { identifier ~ ".$watch.options" ~ "(" ~ watch_option_pair? ~ ("," ~ watch_option_pair)* ~ ","? ~ ")" } +watch_option_pair = { identifier ~ ":" ~ (quoted_string_literal | identifier) } + assign_op = _{ BIT_SHL_ASSIGN | BIT_SHR_ASSIGN | ADD_ASSIGN | SUB_ASSIGN | MUL_ASSIGN | DIV_ASSIGN | MOD_ASSIGN | BIT_AND_ASSIGN | BIT_OR_ASSIGN | BIT_XOR_ASSIGN } ADD_ASSIGN = { "+=" } @@ -449,10 +452,11 @@ while_loop = { "while" ~ condition_and_block } for_loop = { "for" ~ openParen? ~ (iterator_for_loop | c_for_loop) ~ closeParen? ~ expr_block } // Allow optional `let` before the loop variable in iterator-style for loops LET_KEYWORD = { "let" } +WATCH_KEYWORD = { "watch" } iterator_for_loop = { (LET_KEYWORD ~ identifier | identifier) ~ "in" ~ block_aware_tail_expression } c_for_loop = { c_for_init_stmt? ~ ";" ~ expression? ~ ";" ~ c_for_after_stmt? } // NOTE: let_expr ... list copied from `stmt` rule, discarding nonsensical stmts -c_for_init_stmt = { watch_expr | let_expr | assign_op_stmt | assign_stmt | fn_app | generic_fn_app } +c_for_init_stmt = { let_expr | assign_op_stmt | assign_stmt | fn_app | generic_fn_app } // same as above but without `let`. c_for_after_stmt = { block_aware_assign_op_stmt | block_aware_assign_stmt | fn_app | generic_fn_app } @@ -468,9 +472,9 @@ assert_stmt = { "assert" ~ expression } lambda = { named_argument_list ~ "=>" ~ expression } // Class constructors. -// e.g. `new MyClass { x = 1, y = 2 }`. +// e.g. `new MyClass { x = 1, y = 2 }` or `baml.MyClass { x = 1, y = 2 }`. // -class_constructor = { identifier ~ "{" ~ NEWLINE? ~ (class_field_value_pair ~ COMMA? ~ NEWLINE?)* ~ NEWLINE? ~ "}" } +class_constructor = { (path_identifier | identifier) ~ "{" ~ NEWLINE? ~ (class_field_value_pair ~ COMMA? ~ NEWLINE?)* ~ NEWLINE? ~ "}" } // A single field in a class constructor. class_field_value_pair = { (identifier ~ COLON ~ expression) | struct_spread } diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs b/engine/baml-lib/ast/src/parser/parse_expr.rs index 83b6b41259..62ffde4015 100644 --- a/engine/baml-lib/ast/src/parser/parse_expr.rs +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs @@ -34,7 +34,14 @@ pub fn parse_expr_fn(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { only_let_stmt("assert statements", span, diagnostics) } + Stmt::WatchOptions(WatchOptionsStmt { span, .. }) => { + only_let_stmt("watch options statements", span, diagnostics) + } } } @@ -499,13 +509,88 @@ fn parse_statement_inner_rule( finish_assign_op_stmt(span, diagnostics, lhs, op_token, maybe_body).map(Stmt::AssignOp) } - Rule::let_expr | Rule::watch_expr => { - let is_watched = stmt_token.as_rule() == Rule::watch_expr; + Rule::watch_options_stmt => { + let mut tokens = stmt_token.into_inner(); + + // First token is the variable identifier + let variable = parse_identifier(tokens.next()?, diagnostics); + + // Parse watch option pairs (name: "value", when: Function) + let mut name: Option = None; + let mut when: Option = None; + + while let Some(pair_token) = tokens.next() { + if pair_token.as_rule() != Rule::watch_option_pair { + continue; + } + + // Get the span before consuming pair_token + let pair_span = diagnostics.span(pair_token.as_span()); + let mut pair_tokens = pair_token.into_inner(); + let option_name = pair_tokens.next()?.as_str(); + let option_value = pair_tokens.next()?; + + match option_name { + "name" => { + // Must be a string literal + if option_value.as_rule() == Rule::quoted_string_literal { + name = Some(option_value.as_str().trim_matches('"').to_string()); + } else { + diagnostics.push_error(DatamodelError::new_static( + "watch options 'name' must be a string literal", + diagnostics.span(option_value.as_span()), + )); + } + } + "when" => { + // Must be an identifier (function name) + if option_value.as_rule() == Rule::identifier { + when = Some(parse_identifier(option_value, diagnostics)); + } else { + diagnostics.push_error(DatamodelError::new_static( + "watch options 'when' must be a function identifier", + diagnostics.span(option_value.as_span()), + )); + } + } + _ => { + let error_msg = format!( + "unknown watch option '{}'. Valid options are: name, when", + option_name + ); + diagnostics.push_error(DatamodelError::new_validation_error( + &error_msg, + pair_span.clone(), + )); + } + } + } + + Some(Stmt::WatchOptions(WatchOptionsStmt { + variable, + name, + when, + span, + })) + } + Rule::let_expr => { let mut let_binding_tokens = stmt_token.into_inner(); let is_mutable = true; // Always mutable now after mut keyword removal - let identifier = parse_identifier(let_binding_tokens.next()?, diagnostics); + // Check if "watch" keyword is present + let first_token = let_binding_tokens.next()?; + + let (is_watched, identifier) = if first_token.as_rule() == Rule::WATCH_KEYWORD { + // "watch" keyword present, next token is identifier + ( + true, + parse_identifier(let_binding_tokens.next()?, diagnostics), + ) + } else { + // No "watch" keyword, first token is identifier + (false, parse_identifier(first_token, diagnostics)) + }; // Optional type annotation: `: ` // Grammar packs this as a `let_type_annotation` pair if present. @@ -997,6 +1082,9 @@ fn bind_headers_to_statement( Stmt::Assert(_) => { // Assert statements do not carry annotations (for now) } + Stmt::WatchOptions(_) => { + // Watch options statements do not carry annotations + } } } diff --git a/engine/baml-lib/ast/src/parser/parse_expression.rs b/engine/baml-lib/ast/src/parser/parse_expression.rs index 791e942326..d0f32f2857 100644 --- a/engine/baml-lib/ast/src/parser/parse_expression.rs +++ b/engine/baml-lib/ast/src/parser/parse_expression.rs @@ -7,7 +7,7 @@ use super::{ parse_expr_block, parse_expr_fn, parse_fn_app, parse_generic_fn_app, parse_if_expression, parse_lambda, }, - parse_identifier::parse_identifier, + parse_identifier::{parse_identifier, parse_path_identifier}, Rule, }; use crate::{ @@ -627,10 +627,15 @@ pub fn parse_class_constructor(token: Pair<'_>, diagnostics: &mut Diagnostics) - let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); - let class_name = parse_identifier( - tokens.next().expect("Guaranteed by the grammar"), - diagnostics, - ); + let name_token = tokens.next().expect("Guaranteed by the grammar"); + let class_name = match name_token.as_rule() { + Rule::path_identifier => { + // For path identifiers like "baml.WatchOptions", convert to identifier with the full path + parse_path_identifier(name_token, diagnostics) + } + Rule::identifier => parse_identifier(name_token, diagnostics), + _ => unreachable!("Grammar guarantees path_identifier or identifier"), + }; let mut fields = Vec::new(); while let Some(field_or_close_bracket) = tokens.next() { if field_or_close_bracket.as_str() == "}" { diff --git a/engine/baml-lib/baml/tests/hir_files/emit.baml b/engine/baml-lib/baml/tests/hir_files/watch.baml similarity index 89% rename from engine/baml-lib/baml/tests/hir_files/emit.baml rename to engine/baml-lib/baml/tests/hir_files/watch.baml index 2fe5b7a2be..905e026a1b 100644 --- a/engine/baml-lib/baml/tests/hir_files/emit.baml +++ b/engine/baml-lib/baml/tests/hir_files/watch.baml @@ -1,7 +1,7 @@ function Foo() -> int { - watch x = 5; - watch y = 10; - watch z: int = 20; + watch let x = 5; + watch let y = 10; + watch let z: int = 20; 10 } From fae226cda9b249b947551e3a90a5301e735707af Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 20 Oct 2025 12:14:55 -0700 Subject: [PATCH 05/16] save the class constructor in syntax form --- engine/baml-compiler/src/hir.rs | 6 ++ engine/baml-compiler/src/hir/dump.rs | 3 + engine/baml-compiler/src/hir/lowering.rs | 67 +++++++++++++++--- engine/baml-compiler/src/thir/typecheck.rs | 13 ++++ engine/baml-compiler/src/watch.rs | 12 ++-- engine/baml-lib/ast/src/ast.rs | 2 +- .../baml-lib/ast/src/ast/header_collector.rs | 1 + engine/baml-lib/ast/src/ast/mermaid_debug.rs | 4 ++ engine/baml-lib/ast/src/ast/stmt.rs | 64 +++++++++-------- engine/baml-lib/ast/src/parser/datamodel.pest | 12 +++- engine/baml-lib/ast/src/parser/parse_expr.rs | 70 +++++-------------- .../validations/expr_fns.rs | 2 + 12 files changed, 156 insertions(+), 100 deletions(-) diff --git a/engine/baml-compiler/src/hir.rs b/engine/baml-compiler/src/hir.rs index d7a4d89714..6a1d83d959 100644 --- a/engine/baml-compiler/src/hir.rs +++ b/engine/baml-compiler/src/hir.rs @@ -195,6 +195,12 @@ pub enum Statement { when: Option, span: Span, }, + /// Manually notify watchers of a variable. + /// Syntax: `variable.$watch.notify()` + WatchNotify { + variable: String, + span: Span, + }, } #[derive(Clone, Debug)] diff --git a/engine/baml-compiler/src/hir/dump.rs b/engine/baml-compiler/src/hir/dump.rs index 9401884862..45fdbc294b 100644 --- a/engine/baml-compiler/src/hir/dump.rs +++ b/engine/baml-compiler/src/hir/dump.rs @@ -293,6 +293,9 @@ impl Statement { doc = doc.append(RcDoc::intersperse(parts, RcDoc::text(", "))); doc.append(RcDoc::text(");")) } + Statement::WatchNotify { variable, .. } => { + RcDoc::text(variable.clone()).append(RcDoc::text(".$watch.notify();")) + } } } } diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 576656ee1a..82aa234497 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -301,6 +301,52 @@ impl ExprFunction { } } +/// Extract name and when fields from a WatchOptions class constructor expression. +/// Expected expression: baml.WatchOptions{name: "value", when: FunctionName} +fn extract_watch_options_fields(expr: &ast::Expression) -> (Option, Option) { + use ast::Expression; + + // The expression should be a class constructor + if let Expression::ClassConstructor(class_ctor, _) = expr { + let mut name = None; + let mut when = None; + + // Extract field values + for field in &class_ctor.fields { + if let ast::ClassConstructorField::Named(field_name, field_value) = field { + match field_name.name() { + "name" => { + // name should be a string value + if let Expression::StringValue(s, _) = field_value { + name = Some(s.clone()); + } else if let Expression::RawStringValue(raw) = field_value { + name = Some(raw.value().to_string()); + } + } + "when" => { + // when should be an identifier (function name) or string "manual" + match field_value { + Expression::Identifier(id) => { + when = Some(id.name().to_string()); + } + Expression::StringValue(s, _) if s == "manual" => { + when = Some("manual".to_string()); + } + _ => {} + } + } + _ => {} // Ignore unknown fields + } + } + } + + (name, when) + } else { + // If not a class constructor, return empty options + (None, None) + } +} + impl Block { /// Lower an expression block into HIR for expression blocks. pub fn from_expr_block(block: &ast::ExpressionBlock) -> Self { @@ -310,15 +356,13 @@ impl Block { for stmt in &block.stmts { if let ast::Stmt::WatchOptions(ast::WatchOptionsStmt { variable, - name, - when, + options_expr, .. }) = stmt { - watch_options_map.insert( - variable.to_string(), - (name.clone(), when.as_ref().map(|id| id.to_string())), - ); + // Extract name and when from the WatchOptions class constructor expression + let (name, when) = extract_watch_options_fields(options_expr); + watch_options_map.insert(variable.to_string(), (name, when)); } } @@ -327,8 +371,10 @@ impl Block { .stmts .iter() .filter_map(|stmt| { - // Skip WatchOptions statements - they're applied during watch spec creation - if matches!(stmt, ast::Stmt::WatchOptions(_)) { + // Skip WatchOptions and WatchNotify statements + // WatchOptions are applied during watch spec creation + // WatchNotify will be handled separately when we implement manual notifications + if matches!(stmt, ast::Stmt::WatchOptions(_) | ast::Stmt::WatchNotify(_)) { None } else { Some(lower_stmt_with_options(stmt, &watch_options_map)) @@ -539,6 +585,11 @@ fn lower_stmt_with_options( // If we reach here, it's a bug in the lowering logic. unreachable!("WatchOptions statements should not reach lower_stmt_with_options") } + ast::Stmt::WatchNotify(_) => { + // WatchNotify statements should be filtered out during Block::from_expr_block. + // If we reach here, it's a bug in the lowering logic. + unreachable!("WatchNotify statements should not reach lower_stmt_with_options") + } } } diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 3143f5a987..698eb49858 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1190,6 +1190,19 @@ fn typecheck_statement( // that gets processed during watch analysis None } + hir::Statement::WatchNotify { variable, span } => { + // Check that the variable exists in context + if !context.vars.contains_key(variable) { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Unknown variable '{}' in watch notify", variable), + span.clone(), + )); + } + + // Watch notify statements are not included in THIR - they're for runtime + // manual notification of watchers + None + } } } diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 8fc89593ae..6dbb619d86 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -634,17 +634,17 @@ mod tests { r#" function A() -> int { watch let a_1: int = 1; - a_1.$watch.options(name: "a"); + a_1.$watch.options(baml.WatchOptions{name: "a"}); watch let a_2: string = "hi"; - a_2.$watch.options(name: "a"); + a_2.$watch.options(baml.WatchOptions{name: "a"}); watch let b_1: int | bool = true; - b_1.$watch.options(name: "b"); + b_1.$watch.options(baml.WatchOptions{name: "b"}); watch let b_2: int = 3; - b_2.$watch.options(name: "b"); + b_2.$watch.options(baml.WatchOptions{name: "b"}); watch let c_1: int = 1; - c_1.$watch.options(name: "c"); + c_1.$watch.options(baml.WatchOptions{name: "c"}); watch let c_2: int | bool = 3; - c_2.$watch.options(name: "c"); + c_2.$watch.options(baml.WatchOptions{name: "c"}); 1 } "#, diff --git a/engine/baml-lib/ast/src/ast.rs b/engine/baml-lib/ast/src/ast.rs index cfdc0b752f..534e04a91f 100644 --- a/engine/baml-lib/ast/src/ast.rs +++ b/engine/baml-lib/ast/src/ast.rs @@ -45,7 +45,7 @@ pub use mermaid_debug::MermaidDiagramGenerator; pub use newline_type::NewlineType; pub use stmt::{ AssertStmt, AssignOp, AssignOpStmt, AssignStmt, CForLoopStmt, ExprStmt, ForLoopStmt, Header, - LetStmt, ReturnStmt, Stmt, WatchOptionsStmt, WhileStmt, + LetStmt, ReturnStmt, Stmt, WatchNotifyStmt, WatchOptionsStmt, WhileStmt, }; pub use template_string::TemplateString; pub use top::Top; diff --git a/engine/baml-lib/ast/src/ast/header_collector.rs b/engine/baml-lib/ast/src/ast/header_collector.rs index 8ba7f12bb1..6bc045442b 100644 --- a/engine/baml-lib/ast/src/ast/header_collector.rs +++ b/engine/baml-lib/ast/src/ast/header_collector.rs @@ -435,6 +435,7 @@ impl HeaderCollector { } Stmt::Assert(assert_stmt) => self.visit_expression(&assert_stmt.value), Stmt::WatchOptions(_) => {} // Watch options don't contain expressions to visit + Stmt::WatchNotify(_) => {} // Watch notify statements don't contain expressions to visit } } diff --git a/engine/baml-lib/ast/src/ast/mermaid_debug.rs b/engine/baml-lib/ast/src/ast/mermaid_debug.rs index 84902c0cc3..fd7c9367de 100644 --- a/engine/baml-lib/ast/src/ast/mermaid_debug.rs +++ b/engine/baml-lib/ast/src/ast/mermaid_debug.rs @@ -772,6 +772,10 @@ impl MermaidDiagramGenerator { let label = format!("WatchOptions: {}", watch_opts.variable.name()); self.get_node_id_with_class(&key, &label, "statementNode") } + Stmt::WatchNotify(watch_notify) => { + let label = format!("WatchNotify: {}", watch_notify.variable.name()); + self.get_node_id_with_class(&key, &label, "statementNode") + } } } diff --git a/engine/baml-lib/ast/src/ast/stmt.rs b/engine/baml-lib/ast/src/ast/stmt.rs index 00384c8304..667c475540 100644 --- a/engine/baml-lib/ast/src/ast/stmt.rs +++ b/engine/baml-lib/ast/src/ast/stmt.rs @@ -103,15 +103,23 @@ pub struct AssertStmt { } /// Special statement for configuring watch options on a watched variable. -/// Syntax: `variable_name.$watch.options(name: "channel", when: SomeFunction)` +/// Syntax: `variable_name.$watch.options(baml.WatchOptions{name: "channel", when: SomeFunction})` +/// Note: .$watch.options is a special method, but baml.WatchOptions is a real struct #[derive(Debug, Clone)] pub struct WatchOptionsStmt { /// The variable being configured pub variable: Identifier, - /// Optional custom channel name - pub name: Option, - /// Optional filter function for conditional watching - pub when: Option, + /// The WatchOptions constructor expression + pub options_expr: Expression, + pub span: Span, +} + +/// Special statement for manually notifying watchers of a variable. +/// Syntax: `variable_name.$watch.notify()` +#[derive(Debug, Clone)] +pub struct WatchNotifyStmt { + /// The variable to notify watchers about + pub variable: Identifier, pub span: Span, } @@ -133,6 +141,7 @@ pub enum Stmt { Return(ReturnStmt), Assert(AssertStmt), WatchOptions(WatchOptionsStmt), + WatchNotify(WatchNotifyStmt), } impl fmt::Display for AssignOp { @@ -215,21 +224,13 @@ impl fmt::Display for Stmt { Stmt::Assert(AssertStmt { value, .. }) => write!(f, "assert {value}"), Stmt::WatchOptions(WatchOptionsStmt { variable, - name, - when, + options_expr, .. }) => { - write!(f, "{}.$watch.options(", variable.name())?; - if let Some(n) = name { - write!(f, "name: \"{n}\"")?; - } - if let Some(w) = when { - if name.is_some() { - write!(f, ", ")?; - } - write!(f, "when: {}", w.name())?; - } - write!(f, ")") + write!(f, "{}.$watch.options({})", variable.name(), options_expr) + } + Stmt::WatchNotify(WatchNotifyStmt { variable, .. }) => { + write!(f, "{}.$watch.notify()", variable.name()) } } } @@ -329,24 +330,24 @@ impl Stmt { ( Stmt::WatchOptions(WatchOptionsStmt { variable: v1, - name: n1, - when: w1, + options_expr: e1, .. }), Stmt::WatchOptions(WatchOptionsStmt { variable: v2, - name: n2, - when: w2, + options_expr: e2, .. }), ) => { v1.assert_eq_up_to_span(v2); - assert_eq!(n1, n2, "watch option names do not match"); - match (w1, w2) { - (Some(w1), Some(w2)) => w1.assert_eq_up_to_span(w2), - (None, None) => {} - _ => panic!("watch option when clauses do not match"), - } + e1.assert_eq_up_to_span(e2); + } + + ( + Stmt::WatchNotify(WatchNotifyStmt { variable: v1, .. }), + Stmt::WatchNotify(WatchNotifyStmt { variable: v2, .. }), + ) => { + v1.assert_eq_up_to_span(v2); } ( @@ -362,7 +363,8 @@ impl Stmt { | Stmt::Break(_) | Stmt::Continue(_) | Stmt::Assert(_) - | Stmt::WatchOptions(_), + | Stmt::WatchOptions(_) + | Stmt::WatchNotify(_), _, ) => { panic!("Types do not match: {self:?} and {other:?}") @@ -398,6 +400,7 @@ impl Stmt { ), }, Stmt::WatchOptions(WatchOptionsStmt { variable, .. }) => variable, + Stmt::WatchNotify(WatchNotifyStmt { variable, .. }) => variable, } } @@ -413,7 +416,8 @@ impl Stmt { | Stmt::Break(span) | Stmt::Continue(span) | Stmt::Assert(AssertStmt { span, .. }) - | Stmt::WatchOptions(WatchOptionsStmt { span, .. }) => span, + | Stmt::WatchOptions(WatchOptionsStmt { span, .. }) + | Stmt::WatchNotify(WatchNotifyStmt { span, .. }) => span, Stmt::Expression(es) => &es.span, Stmt::Semicolon(expr) => expr.span(), diff --git a/engine/baml-lib/ast/src/parser/datamodel.pest b/engine/baml-lib/ast/src/parser/datamodel.pest index 2cd68ceabb..381455c351 100644 --- a/engine/baml-lib/ast/src/parser/datamodel.pest +++ b/engine/baml-lib/ast/src/parser/datamodel.pest @@ -348,6 +348,7 @@ expr_body_stmt = { ( let_expr | watch_options_stmt + | watch_notify_stmt | assign_op_stmt | assign_stmt ) @@ -373,6 +374,7 @@ stmt = { | return_stmt | assert_stmt | watch_options_stmt + | watch_notify_stmt | assign_op_stmt | assign_stmt | expression @@ -407,9 +409,13 @@ assign_stmt = { expression ~ "=" ~ expression } assign_op_stmt = { expression ~ assign_op ~ expression } // Special syntax for configuring watch options on a watched variable -// e.g. variable.$watch.options(name: "channel", when: FilterFunc) -watch_options_stmt = { identifier ~ ".$watch.options" ~ "(" ~ watch_option_pair? ~ ("," ~ watch_option_pair)* ~ ","? ~ ")" } -watch_option_pair = { identifier ~ ":" ~ (quoted_string_literal | identifier) } +// e.g. variable.$watch.options(baml.WatchOptions{name: "channel", when: FilterFunc}) +// Note: .$watch.options is a special method, but baml.WatchOptions is a real struct +watch_options_stmt = { identifier ~ ".$watch.options" ~ "(" ~ expression ~ ")" } + +// Special syntax for manually notifying watchers of a variable +// e.g. variable.$watch.notify() +watch_notify_stmt = { identifier ~ ".$watch.notify" ~ "(" ~ ")" } assign_op = _{ BIT_SHL_ASSIGN | BIT_SHR_ASSIGN | ADD_ASSIGN | SUB_ASSIGN | MUL_ASSIGN | DIV_ASSIGN | MOD_ASSIGN | BIT_AND_ASSIGN | BIT_OR_ASSIGN | BIT_XOR_ASSIGN } diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs b/engine/baml-lib/ast/src/parser/parse_expr.rs index 62ffde4015..65e362e838 100644 --- a/engine/baml-lib/ast/src/parser/parse_expr.rs +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs @@ -129,6 +129,9 @@ pub fn parse_top_level_assignment( Stmt::WatchOptions(WatchOptionsStmt { span, .. }) => { only_let_stmt("watch options statements", span, diagnostics) } + Stmt::WatchNotify(WatchNotifyStmt { span, .. }) => { + only_let_stmt("watch notify statements", span, diagnostics) + } } } @@ -515,64 +518,24 @@ fn parse_statement_inner_rule( // First token is the variable identifier let variable = parse_identifier(tokens.next()?, diagnostics); - // Parse watch option pairs (name: "value", when: Function) - let mut name: Option = None; - let mut when: Option = None; - - while let Some(pair_token) = tokens.next() { - if pair_token.as_rule() != Rule::watch_option_pair { - continue; - } - - // Get the span before consuming pair_token - let pair_span = diagnostics.span(pair_token.as_span()); - let mut pair_tokens = pair_token.into_inner(); - let option_name = pair_tokens.next()?.as_str(); - let option_value = pair_tokens.next()?; - - match option_name { - "name" => { - // Must be a string literal - if option_value.as_rule() == Rule::quoted_string_literal { - name = Some(option_value.as_str().trim_matches('"').to_string()); - } else { - diagnostics.push_error(DatamodelError::new_static( - "watch options 'name' must be a string literal", - diagnostics.span(option_value.as_span()), - )); - } - } - "when" => { - // Must be an identifier (function name) - if option_value.as_rule() == Rule::identifier { - when = Some(parse_identifier(option_value, diagnostics)); - } else { - diagnostics.push_error(DatamodelError::new_static( - "watch options 'when' must be a function identifier", - diagnostics.span(option_value.as_span()), - )); - } - } - _ => { - let error_msg = format!( - "unknown watch option '{}'. Valid options are: name, when", - option_name - ); - diagnostics.push_error(DatamodelError::new_validation_error( - &error_msg, - pair_span.clone(), - )); - } - } - } + // Second token is the WatchOptions expression (should be a class constructor) + let options_expr_token = tokens.next()?; + let options_expr = parse_expression(options_expr_token, diagnostics)?; Some(Stmt::WatchOptions(WatchOptionsStmt { variable, - name, - when, + options_expr, span, })) } + Rule::watch_notify_stmt => { + let mut tokens = stmt_token.into_inner(); + + // Only token is the variable identifier + let variable = parse_identifier(tokens.next()?, diagnostics); + + Some(Stmt::WatchNotify(WatchNotifyStmt { variable, span })) + } Rule::let_expr => { let mut let_binding_tokens = stmt_token.into_inner(); @@ -1085,6 +1048,9 @@ fn bind_headers_to_statement( Stmt::WatchOptions(_) => { // Watch options statements do not carry annotations } + Stmt::WatchNotify(_) => { + // Watch notify statements do not carry annotations + } } } diff --git a/engine/baml-lib/baml-core/src/validate/validation_pipeline/validations/expr_fns.rs b/engine/baml-lib/baml-core/src/validate/validation_pipeline/validations/expr_fns.rs index 84d5b1c1f1..fe1c24be44 100644 --- a/engine/baml-lib/baml-core/src/validate/validation_pipeline/validations/expr_fns.rs +++ b/engine/baml-lib/baml-core/src/validate/validation_pipeline/validations/expr_fns.rs @@ -257,6 +257,8 @@ fn validate_stmt(ctx: &mut Context<'_>, stmt: &Stmt, scope: &HashSet) { Stmt::Return(ReturnStmt { value, .. }) | Stmt::Assert(AssertStmt { value, .. }) => { validate_expression(ctx, value, scope); } + Stmt::WatchNotify(_) => {} + Stmt::WatchOptions(_) => {} } } From e8a00752f18ba69f68d025de25b62520efe9cc80 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 20 Oct 2025 15:32:35 -0700 Subject: [PATCH 06/16] check names of constructed classes --- engine/baml-compiler/src/builtin.rs | 12 ++++---- engine/baml-compiler/src/thir/interpret.rs | 2 +- engine/baml-compiler/src/thir/typecheck.rs | 18 +++++++++-- .../tests/bytecode_files/array_access.baml | 6 ++-- .../baml/tests/bytecode_files/assert.baml | 6 ++-- .../tests/bytecode_files/function_calls.baml | 6 ++-- .../tests/bytecode_files/literal_values.baml | 6 ++-- .../tests/bytecode_files/llm_functions.baml | 6 ++-- .../tests/bytecode_files/loops/break.baml | 6 ++-- .../tests/bytecode_files/loops/c_for.baml | 6 ++-- .../tests/bytecode_files/loops/continue.baml | 6 ++-- .../baml/tests/bytecode_files/loops/for.baml | 6 ++-- .../bytecode_files/loops/while_loops.baml | 6 ++-- .../baml/tests/bytecode_files/maps.baml | 6 ++-- .../tests/bytecode_files/mut_variables.baml | 6 ++-- .../bytecode_files/return_statement.baml | 6 ++-- .../tests/bytecode_files/simple_function.baml | 6 ++-- .../baml/tests/hir_files/array_and_call.baml | 6 ++-- .../baml-lib/baml/tests/hir_files/assert.baml | 6 ++-- .../baml/tests/hir_files/basic_class.baml | 6 ++-- .../baml/tests/hir_files/classes.baml | 6 ++-- .../baml/tests/hir_files/enum_example.baml | 6 ++-- .../tests/hir_files/expression_with_let.baml | 6 ++-- .../tests/hir_files/if_expression_let.baml | 6 ++-- .../baml/tests/hir_files/literal_values.baml | 6 ++-- .../baml/tests/hir_files/llm_functions.baml | 6 ++-- .../baml/tests/hir_files/loops/break.baml | 6 ++-- .../baml/tests/hir_files/loops/c_for.baml | 6 ++-- .../baml/tests/hir_files/loops/continue.baml | 6 ++-- .../baml/tests/hir_files/loops/for.baml | 6 ++-- .../tests/hir_files/loops/while_loops.baml | 6 ++-- .../baml-lib/baml/tests/hir_files/maps.baml | 6 ++-- .../baml/tests/hir_files/mut_variables.baml | 6 ++-- .../nested_types_with_attributes.baml | 6 ++-- .../baml/tests/hir_files/operators.baml | 6 ++-- .../tests/hir_files/return_statement.baml | 6 ++-- .../hir_files/streaming_and_constraints.baml | 6 ++-- .../baml-lib/baml/tests/hir_files/watch.baml | 6 ++-- .../tests/validation_files/expr/builtin.baml | 28 ++++++++--------- .../expr/constructors_invalid.baml | 30 +++++++++---------- .../baml_src/test-files/builtin/fetch.baml | 2 +- 41 files changed, 157 insertions(+), 145 deletions(-) diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index 75d928ab2f..c4dcad07de 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -8,12 +8,12 @@ pub mod functions { } pub mod classes { - pub const REQUEST: &str = "std::Request"; - pub const WATCH_OPTIONS: &str = "baml::WatchOptions"; + pub const REQUEST: &str = "std.Request"; + pub const WATCH_OPTIONS: &str = "baml.WatchOptions"; } pub mod enums { - pub const HTTP_METHOD: &str = "std::HttpMethod"; + pub const HTTP_METHOD: &str = "std.HttpMethod"; } pub fn builtin_classes() -> Vec { @@ -46,18 +46,18 @@ pub fn builtin_classes() -> Vec { fields: vec![ Field { name: String::from("name"), - r#type: TypeIR::string(), + r#type: TypeIR::optional(TypeIR::string()), span: Span::fake(), }, Field { name: String::from("when"), // "never" | "manual" | ((T, T) -> bool) // We use a generic function type with top types for T - r#type: TypeIR::union(vec![ + r#type: TypeIR::optional(TypeIR::union(vec![ TypeIR::literal_string("never".to_string()), TypeIR::literal_string("manual".to_string()), TypeIR::arrow(vec![TypeIR::top(), TypeIR::top()], TypeIR::bool()), - ]), + ])), span: Span::fake(), }, ], diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index 8659da7c32..b8dbb577e1 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -2028,7 +2028,7 @@ where Builtin::FetchValue => { // FetchValue requires network access and is not supported in the interpreter bail!( - "builtin function std::fetch_value is not supported in interpreter at {:?}", + "builtin function std.fetch_value is not supported in interpreter at {:?}", meta.0 ) } diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 698eb49858..b7ee2b3af2 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1481,7 +1481,7 @@ pub fn typecheck_expression( // TODO: Handle generics uniformly, not with this kind of one-off handler. if func_name == crate::builtin::functions::FETCH_AS && type_args.is_empty() { diagnostics.push_error(DatamodelError::new_validation_error( - "Generic function std::fetch_value must have a type argument. Try adding a type argument like this: std::fetch_value", + "Generic function std.fetch_value must have a type argument. Try adding a type argument like this: std.fetch_value", function.span().clone(), )); } @@ -2044,7 +2044,13 @@ pub fn typecheck_expression( let mut typed_fields = Vec::new(); // Look up class definition to validate fields - let class_def = context.classes.get(&constructor.class_name).cloned(); + // Normalize class name: try both dot and :: separators (baml.WatchOptions vs baml::WatchOptions) + let normalized_class_name = constructor.class_name.replace('.', "::"); + let class_def = context + .classes + .get(&constructor.class_name) + .or_else(|| context.classes.get(&normalized_class_name)) + .cloned(); if let Some(class_def) = class_def { // Create a map of field names to types @@ -2189,7 +2195,13 @@ pub fn typecheck_expression( } } } else { - // If we don't have the class def, validate each field anyway + // Class doesn't exist - report an error + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Unknown class '{}'", constructor.class_name), + span.clone(), + )); + + // Still typecheck the fields to catch any additional errors for field in &constructor.fields { match field { hir::ClassConstructorField::Named { name, value } => { diff --git a/engine/baml-lib/baml/tests/bytecode_files/array_access.baml b/engine/baml-lib/baml/tests/bytecode_files/array_access.baml index 1fd19c73ad..cd09535c48 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/array_access.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/array_access.baml @@ -44,9 +44,9 @@ function ArrayAccessWithVariable(arr: float[], idx: int) -> float { // 15 3 LOAD_VAR 3 (result) // 4 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/assert.baml b/engine/baml-lib/baml/tests/bytecode_files/assert.baml index 10859bb523..5ada8e3ecd 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/assert.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/assert.baml @@ -31,9 +31,9 @@ function assertNotOk() -> int { // 10 4 LOAD_CONST 2 (2) // 5 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml b/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml index f9fbd4ebbb..b3026a3bd6 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/function_calls.baml @@ -52,9 +52,9 @@ function Nested(x: int) -> int { // 9 CALL 2 // 10 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml b/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml index 9efb763acf..628f366715 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/literal_values.baml @@ -45,9 +45,9 @@ function ReturnArray() -> int[] { // 5 ALLOC_ARRAY 5 // 6 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml b/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml index a3f960a7d2..ff40811f0c 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/llm_functions.baml @@ -35,9 +35,9 @@ function AnalyzeSentiment(text: string) -> Sentiment { // // Function: AnalyzeSentiment // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Enum Sentiment // Function: baml.Array.length // diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml index 428ef81e6f..3fafacf560 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/break.baml @@ -89,9 +89,9 @@ function Nested() -> int { // 27 21 LOAD_VAR 1 (a) // 22 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml index 08d63cf0cf..141232f13f 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/c_for.baml @@ -129,9 +129,9 @@ function Nothing() -> int { // 44 3 LOAD_VAR 1 (s) // 4 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml index 9694fe3289..65e2bdbf8f 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/continue.baml @@ -92,9 +92,9 @@ function ContinueNested() -> int { // 31 17 LOAD_CONST 3 (5) // 18 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml index 290804dbf9..2d871dd638 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/for.baml @@ -223,9 +223,9 @@ function NestedFor(as: int[], bs: int[]) -> int { // 46 49 LOAD_VAR 3 (result) // 50 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml b/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml index 2bcffe0e80..6e98161fcb 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/loops/while_loops.baml @@ -126,9 +126,9 @@ function WhileWithScopes() -> int { // 51 32 LOAD_CONST 8 (3) // 33 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/maps.baml b/engine/baml-lib/baml/tests/bytecode_files/maps.baml index 6c5e3b615e..068c9de077 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/maps.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/maps.baml @@ -138,9 +138,9 @@ function Len() -> int { // 4 CALL 1 // 5 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml b/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml index ca0009615e..c79c91d322 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/mut_variables.baml @@ -28,9 +28,9 @@ function MutableInArg(x: int) -> int { // 11 2 LOAD_VAR 1 (x) // 3 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml b/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml index e6c58640ac..8b1ecbaeea 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/return_statement.baml @@ -99,9 +99,9 @@ function WithStack(x: int) -> int { // 30 38 LOAD_CONST 9 (7) // 39 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml b/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml index 1c7a565450..c72270b07d 100644 --- a/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml +++ b/engine/baml-lib/baml/tests/bytecode_files/simple_function.baml @@ -7,9 +7,9 @@ function GetName(person: string) -> string { // 1 0 LOAD_CONST 0 (Antonio) // 1 RETURN // -// Class: std::Request with 3 fields -// Class: baml::WatchOptions with 2 fields -// Enum std::HttpMethod +// Class: std.Request with 3 fields +// Class: baml.WatchOptions with 2 fields +// Enum std.HttpMethod // Function: baml.Array.length // // Function: baml.Map.length diff --git a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml index d02f7cacea..c29848af6f 100644 --- a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml +++ b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml @@ -30,17 +30,17 @@ function ArrayAccess() -> int { // arr[1] // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/assert.baml b/engine/baml-lib/baml/tests/hir_files/assert.baml index d224360a98..f563f8a552 100644 --- a/engine/baml-lib/baml/tests/hir_files/assert.baml +++ b/engine/baml-lib/baml/tests/hir_files/assert.baml @@ -23,17 +23,17 @@ function assertNotOk() -> int { // 2 // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/basic_class.baml b/engine/baml-lib/baml/tests/hir_files/basic_class.baml index bb5648fd00..5bc3a5b0e7 100644 --- a/engine/baml-lib/baml/tests/hir_files/basic_class.baml +++ b/engine/baml-lib/baml/tests/hir_files/basic_class.baml @@ -3,13 +3,13 @@ class Person { age int } -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } @@ -19,6 +19,6 @@ class Person { // age: int // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/classes.baml b/engine/baml-lib/baml/tests/hir_files/classes.baml index c5656ddd2c..e1eb8445e0 100644 --- a/engine/baml-lib/baml/tests/hir_files/classes.baml +++ b/engine/baml-lib/baml/tests/hir_files/classes.baml @@ -18,13 +18,13 @@ function TestClass() -> int { // example.a // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } @@ -34,6 +34,6 @@ function TestClass() -> int { // b: string // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/enum_example.baml b/engine/baml-lib/baml/tests/hir_files/enum_example.baml index 41fbc445f5..6aa580130a 100644 --- a/engine/baml-lib/baml/tests/hir_files/enum_example.baml +++ b/engine/baml-lib/baml/tests/hir_files/enum_example.baml @@ -9,13 +9,13 @@ class Widget { size int } -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } @@ -25,7 +25,7 @@ class Widget { // size: int // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } // diff --git a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml index aca8f5847a..95b68576d0 100644 --- a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml @@ -9,17 +9,17 @@ function AddOne(x: int) -> int { // y // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml index 145e633e9c..3a3afea1e5 100644 --- a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml @@ -15,17 +15,17 @@ function simpleIf() -> string { // x // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/literal_values.baml b/engine/baml-lib/baml/tests/hir_files/literal_values.baml index ade856599f..b5b7980a40 100644 --- a/engine/baml-lib/baml/tests/hir_files/literal_values.baml +++ b/engine/baml-lib/baml/tests/hir_files/literal_values.baml @@ -39,17 +39,17 @@ function ReturnArray() -> int[] { // [1, 2, 3, 4, 5] // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml index 4178513c43..283caf435b 100644 --- a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml +++ b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml @@ -60,18 +60,18 @@ function AnalyzeSentiment(text: string) -> Sentiment { // prompt "Analyze the sentiment of this text: {{ text }}\n\nRespond with exactly one of: POSITIVE, NEGATIVE, NEUTRAL" // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } // diff --git a/engine/baml-lib/baml/tests/hir_files/loops/break.baml b/engine/baml-lib/baml/tests/hir_files/loops/break.baml index 119e5ed1ed..16d7e45092 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/break.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/break.baml @@ -55,17 +55,17 @@ function Nested() -> int { // a // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml index 3346f57eb7..c62f22d038 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml @@ -94,17 +94,17 @@ function Nothing() -> int { // s // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml index 905bd7abe0..0a86ff0d37 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml @@ -52,17 +52,17 @@ function ContinueNested() -> int { // } // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/for.baml b/engine/baml-lib/baml/tests/hir_files/loops/for.baml index 5072386663..f6b8f14f2e 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/for.baml @@ -17,17 +17,17 @@ function Sum(xs: int[]) -> int { // result // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml index 3e88ece3b2..4cb40663fd 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml @@ -23,17 +23,17 @@ function GCD(a: int, b: int) -> int { // a // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/maps.baml b/engine/baml-lib/baml/tests/hir_files/maps.baml index 682ae9cff5..0173ff0518 100644 --- a/engine/baml-lib/baml/tests/hir_files/maps.baml +++ b/engine/baml-lib/baml/tests/hir_files/maps.baml @@ -85,17 +85,17 @@ function Len() -> int { // map.length() // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml index 46b7e840da..9221f96e2e 100644 --- a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml +++ b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml @@ -25,17 +25,17 @@ function MutableInArg(x: int) -> int { // x // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml index 2de6b50709..07f0527989 100644 --- a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml +++ b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml @@ -5,13 +5,13 @@ class DataModel { config map @stream.not_null } -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } @@ -23,6 +23,6 @@ class DataModel { // config: map @stream.needed // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/operators.baml b/engine/baml-lib/baml/tests/hir_files/operators.baml index fd2baa98c4..fed0111b0e 100644 --- a/engine/baml-lib/baml/tests/hir_files/operators.baml +++ b/engine/baml-lib/baml/tests/hir_files/operators.baml @@ -10,17 +10,17 @@ function nestedOps() -> int { // x // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/return_statement.baml b/engine/baml-lib/baml/tests/hir_files/return_statement.baml index 48490a27d2..85d1f4e8a8 100644 --- a/engine/baml-lib/baml/tests/hir_files/return_statement.baml +++ b/engine/baml-lib/baml/tests/hir_files/return_statement.baml @@ -63,17 +63,17 @@ function WithStack(x: int) -> int { // 7 // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml index c6e015baf9..5f2ecfe5de 100644 --- a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml +++ b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml @@ -9,13 +9,13 @@ class User { age int @check(valid_age, {{ this >= 0 }}) } -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } @@ -31,6 +31,6 @@ class User { // age: int @constrained // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/hir_files/watch.baml b/engine/baml-lib/baml/tests/hir_files/watch.baml index 905e026a1b..c6c65b2d13 100644 --- a/engine/baml-lib/baml/tests/hir_files/watch.baml +++ b/engine/baml-lib/baml/tests/hir_files/watch.baml @@ -22,17 +22,17 @@ function MyFunction(prev: int, next: int) -> bool { // true // } // -// class std::Request { +// class std.Request { // base_url: string // headers: map // query_params: map // } // -// class baml::WatchOptions { +// class baml.WatchOptions { // name: string // when: ("never" | "manual" | (ANY, ANY) -> bool) // } // -// enum std::HttpMethod { +// enum std.HttpMethod { // Get // } diff --git a/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml b/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml index 02f3bca053..84b5e719c5 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml @@ -6,7 +6,7 @@ class Todo { } function GetTodo() -> Todo { - std::fetch_value(std::Request { + std.fetch_value(std.Request { base_url: "https://dummyjson.com/todos/1", headers: {}, query_params: {}, @@ -14,40 +14,40 @@ function GetTodo() -> Todo { } function GetTodoMissingTypeArg() -> Todo { - std::fetch_value(std::Request { + std.fetch_value(std.Request { base_url: "https://dummyjson.com/todos/1", headers: {}, query_params: {}, }) } -// error: Generic function std::fetch_value must have a type argument. Try adding a type argument like this: std::fetch_value +// error: Generic function std.fetch_value must have a type argument. Try adding a type argument like this: std.fetch_value // --> expr/builtin.baml:17 -// | +// | // 16 | function GetTodoMissingTypeArg() -> Todo { -// 17 | std::fetch_value(std::Request { +// 17 | std.fetch_value(std.Request { // 18 | base_url: "https://dummyjson.com/todos/1", // 19 | headers: {}, // 20 | query_params: {}, // 21 | }) -// | -// error: Error validating: Unknown function std::fetch_value +// | +// error: Error validating: Unknown function std.fetch_value // --> expr/builtin.baml:9 -// | +// | // 8 | function GetTodo() -> Todo { -// 9 | std::fetch_value(std::Request { +// 9 | std.fetch_value(std.Request { // 10 | base_url: "https://dummyjson.com/todos/1", // 11 | headers: {}, // 12 | query_params: {}, // 13 | }) -// | -// error: Error validating: Unknown function std::fetch_value +// | +// error: Error validating: Unknown function std.fetch_value // --> expr/builtin.baml:17 -// | +// | // 16 | function GetTodoMissingTypeArg() -> Todo { -// 17 | std::fetch_value(std::Request { +// 17 | std.fetch_value(std.Request { // 18 | base_url: "https://dummyjson.com/todos/1", // 19 | headers: {}, // 20 | query_params: {}, // 21 | }) -// | +// | diff --git a/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml b/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml index 1bb6ca6be7..2f2cf77ed5 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml @@ -5,12 +5,12 @@ class Bar { function Foo() -> Bar { let x = Bar { a: "hello", c: 12 }; - let req_bad = std::Request { + let req_bad = std.Request { base_url: 10, headers: {}, query_params: { a 10 }, }; - let req_good = std::Request { + let req_good = std.Request { base_url: "https://example.com", headers: { Authorization "Bearer mytoken" }, query_params: { foo "bar" }, @@ -20,31 +20,31 @@ function Foo() -> Bar { // error: Error validating: Bar.a expected type int, but found string // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | +// | // error: Error validating: Class Bar has no field c // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | +// | // error: Error validating: Class Bar is missing fields: b // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | -// error: Error validating: std::Request.base_url expected type string, but found int +// | +// error: Error validating: std.Request.base_url expected type string, but found int // --> expr/constructors_invalid.baml:9 -// | -// 8 | let req_bad = std::Request { +// | +// 8 | let req_bad = std.Request { // 9 | base_url: 10, -// | -// error: Error validating: std::Request.query_params expected type map, but found map +// | +// error: Error validating: std.Request.query_params expected type map, but found map // --> expr/constructors_invalid.baml:11 -// | +// | // 10 | headers: {}, // 11 | query_params: { a 10 }, -// | +// | diff --git a/integ-tests/baml_src/test-files/builtin/fetch.baml b/integ-tests/baml_src/test-files/builtin/fetch.baml index 801eda87a0..83bc74c242 100644 --- a/integ-tests/baml_src/test-files/builtin/fetch.baml +++ b/integ-tests/baml_src/test-files/builtin/fetch.baml @@ -27,7 +27,7 @@ // } // function SearchWikipedia(query: string) -> string { - // std::fetch_value(std::Request { + // std.fetch_value(std.Request { // base_url: UrlEncode("https://en.wikipedia.org/wiki/Special:Search", {search query}) // }) // } From 3342ebe169f91a20b82b31af6568b2529dbfb6e3 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 20 Oct 2025 15:44:35 -0700 Subject: [PATCH 07/16] check names of constructed classes --- engine/baml-compiler/src/builtin.rs | 10 + engine/baml-compiler/src/codegen.rs | 4 + engine/baml-compiler/src/hir.rs | 4 +- engine/baml-compiler/src/hir/dump.rs | 12 +- engine/baml-compiler/src/hir/lowering.rs | 40 ++- engine/baml-compiler/src/thir.rs | 36 +++ engine/baml-compiler/src/thir/typecheck.rs | 28 +-- engine/baml-compiler/src/watch.rs | 228 +++++++++--------- engine/baml-compiler/tests/builtins.rs | 4 +- engine/baml-compiler/tests/classes.rs | 22 +- engine/baml-compiler/tests/control_flow.rs | 10 +- engine/baml-compiler/tests/enums.rs | 4 +- engine/baml-compiler/tests/maps.rs | 4 +- engine/baml-compiler/tests/watch.rs | 2 +- .../baml/tests/hir_files/array_and_call.baml | 15 -- .../baml-lib/baml/tests/hir_files/assert.baml | 15 -- .../baml/tests/hir_files/basic_class.baml | 15 -- .../baml/tests/hir_files/classes.baml | 15 -- .../baml/tests/hir_files/enum_example.baml | 15 -- .../tests/hir_files/expression_with_let.baml | 15 -- .../tests/hir_files/if_expression_let.baml | 15 -- .../baml/tests/hir_files/literal_values.baml | 15 -- .../baml/tests/hir_files/llm_functions.baml | 15 -- .../baml/tests/hir_files/loops/break.baml | 15 -- .../baml/tests/hir_files/loops/c_for.baml | 15 -- .../baml/tests/hir_files/loops/continue.baml | 15 -- .../baml/tests/hir_files/loops/for.baml | 15 -- .../tests/hir_files/loops/while_loops.baml | 15 -- .../baml-lib/baml/tests/hir_files/maps.baml | 15 -- .../baml/tests/hir_files/mut_variables.baml | 15 -- .../nested_types_with_attributes.baml | 15 -- .../baml/tests/hir_files/operators.baml | 15 -- .../tests/hir_files/return_statement.baml | 15 -- .../hir_files/streaming_and_constraints.baml | 15 -- .../baml-lib/baml/tests/hir_files/watch.baml | 15 -- .../baml-runtime/src/cli/dump_intermediate.rs | 59 +++-- engine/baml-runtime/src/cli/repl.rs | 10 + engine/baml-vm/tests/builtins.rs | 2 +- engine/baml-vm/tests/watch.rs | 4 +- 39 files changed, 276 insertions(+), 522 deletions(-) diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index c4dcad07de..e6d4a1107d 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -90,6 +90,16 @@ pub fn baml_fetch_as_signature(return_type: TypeIR) -> TypeIR { pub fn is_builtin_identifier(identifier: &str) -> bool { identifier.starts_with("std::") || identifier.starts_with("baml::") + || identifier.starts_with("std.") + || identifier.starts_with("baml.") || identifier == "true" || identifier == "false" } + +pub fn is_builtin_class(class_name: &str) -> bool { + class_name == classes::REQUEST || class_name == classes::WATCH_OPTIONS +} + +pub fn is_builtin_enum(enum_name: &str) -> bool { + enum_name == enums::HTTP_METHOD +} diff --git a/engine/baml-compiler/src/codegen.rs b/engine/baml-compiler/src/codegen.rs index 4c532d96f9..d5cbd1a3ba 100644 --- a/engine/baml-compiler/src/codegen.rs +++ b/engine/baml-compiler/src/codegen.rs @@ -906,6 +906,10 @@ impl<'g> HirCompiler<'g> { self.compile_expression(condition); self.emit(Instruction::Assert); } + thir::Statement::WatchOptions { .. } | thir::Statement::WatchNotify { .. } => { + // These are handled at the interpreter level, not in bytecode + // They update runtime watch specs and don't need bytecode generation + } } } diff --git a/engine/baml-compiler/src/hir.rs b/engine/baml-compiler/src/hir.rs index 6a1d83d959..fecc58a41a 100644 --- a/engine/baml-compiler/src/hir.rs +++ b/engine/baml-compiler/src/hir.rs @@ -37,8 +37,8 @@ impl Hir { Hir { expr_functions: vec![], llm_functions: vec![], - classes: vec![], - enums: vec![], + classes: crate::builtin::builtin_classes(), + enums: crate::builtin::builtin_enums(), global_assignments: baml_types::BamlMap::new(), } } diff --git a/engine/baml-compiler/src/hir/dump.rs b/engine/baml-compiler/src/hir/dump.rs index 45fdbc294b..40f40b568e 100644 --- a/engine/baml-compiler/src/hir/dump.rs +++ b/engine/baml-compiler/src/hir/dump.rs @@ -24,13 +24,17 @@ impl Hir { for func in &self.llm_functions { docs.push(func.to_doc()); } - // Add classes + // Add classes (excluding builtins) for class in &self.classes { - docs.push(class.to_doc()); + if !crate::builtin::is_builtin_class(&class.name) { + docs.push(class.to_doc()); + } } - // Add enums + // Add enums (excluding builtins) for enum_def in &self.enums { - docs.push(enum_def.to_doc()); + if !crate::builtin::is_builtin_enum(&enum_def.name) { + docs.push(enum_def.to_doc()); + } } if docs.is_empty() { RcDoc::nil() diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 82aa234497..46eea06e3f 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -366,20 +366,11 @@ impl Block { } } - // Second pass: lower statements, filtering out WatchOptions and applying them to watch specs + // Second pass: lower statements, applying watch options to watch specs let statements: Vec = block .stmts .iter() - .filter_map(|stmt| { - // Skip WatchOptions and WatchNotify statements - // WatchOptions are applied during watch spec creation - // WatchNotify will be handled separately when we implement manual notifications - if matches!(stmt, ast::Stmt::WatchOptions(_) | ast::Stmt::WatchNotify(_)) { - None - } else { - Some(lower_stmt_with_options(stmt, &watch_options_map)) - } - }) + .map(|stmt| lower_stmt_with_options(stmt, &watch_options_map)) .collect(); Block { @@ -579,17 +570,24 @@ fn lower_stmt_with_options( condition: Expression::from_ast(value), span: span.clone(), }, - ast::Stmt::WatchOptions(_) => { - // WatchOptions statements should be filtered out during Block::from_expr_block - // and their settings applied to the WatchSpec of the watched variable. - // If we reach here, it's a bug in the lowering logic. - unreachable!("WatchOptions statements should not reach lower_stmt_with_options") - } - ast::Stmt::WatchNotify(_) => { - // WatchNotify statements should be filtered out during Block::from_expr_block. - // If we reach here, it's a bug in the lowering logic. - unreachable!("WatchNotify statements should not reach lower_stmt_with_options") + ast::Stmt::WatchOptions(ast::WatchOptionsStmt { + variable, + options_expr, + span, + }) => { + // Extract name and when from the WatchOptions expression + let (name, when) = extract_watch_options_fields(options_expr); + Statement::WatchOptions { + variable: variable.to_string(), + name, + when, + span: span.clone(), + } } + ast::Stmt::WatchNotify(ast::WatchNotifyStmt { variable, span }) => Statement::WatchNotify { + variable: variable.to_string(), + span: span.clone(), + }, } } diff --git a/engine/baml-compiler/src/thir.rs b/engine/baml-compiler/src/thir.rs index 215597ef08..29444285ed 100644 --- a/engine/baml-compiler/src/thir.rs +++ b/engine/baml-compiler/src/thir.rs @@ -706,6 +706,20 @@ pub enum Statement { condition: Expr, span: Span, }, + + /// Configure watch options for a watched variable. + WatchOptions { + variable: String, + name: Option, + when: Option, + span: Span, + }, + + /// Manually notify watchers of a variable. + WatchNotify { + variable: String, + span: Span, + }, } impl Statement { @@ -798,6 +812,24 @@ impl Statement { Statement::Assert { condition, .. } => { format!("assert {cond}", cond = condition.dump_str()) } + Statement::WatchOptions { + variable, + name, + when, + .. + } => { + let mut parts = vec![]; + if let Some(n) = name { + parts.push(format!("name: \"{}\"", n)); + } + if let Some(w) = when { + parts.push(format!("when: {}", w)); + } + format!("{}.$watch.options({{{}}})", variable, parts.join(", ")) + } + Statement::WatchNotify { variable, .. } => { + format!("{}.$watch.notify()", variable) + } } } @@ -853,6 +885,10 @@ impl Statement { block_vars } Statement::Assert { condition, .. } => condition.variables(), + Statement::WatchOptions { .. } | Statement::WatchNotify { .. } => { + // These don't reference variables themselves + HashSet::new() + } } } } diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index b7ee2b3af2..4c42975e5c 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1171,8 +1171,8 @@ fn typecheck_statement( } hir::Statement::WatchOptions { variable, - name: _, - when: _, + name, + when, span, } => { // Check that the variable exists in context @@ -1186,9 +1186,12 @@ fn typecheck_statement( // TODO: Validate that 'when' function exists and has correct signature // For now, we just pass it through - // Watch options statements are not included in THIR - they're metadata - // that gets processed during watch analysis - None + Some(thir::Statement::WatchOptions { + variable: variable.clone(), + name: name.clone(), + when: when.clone(), + span: span.clone(), + }) } hir::Statement::WatchNotify { variable, span } => { // Check that the variable exists in context @@ -1199,9 +1202,10 @@ fn typecheck_statement( )); } - // Watch notify statements are not included in THIR - they're for runtime - // manual notification of watchers - None + Some(thir::Statement::WatchNotify { + variable: variable.clone(), + span: span.clone(), + }) } } } @@ -2044,13 +2048,7 @@ pub fn typecheck_expression( let mut typed_fields = Vec::new(); // Look up class definition to validate fields - // Normalize class name: try both dot and :: separators (baml.WatchOptions vs baml::WatchOptions) - let normalized_class_name = constructor.class_name.replace('.', "::"); - let class_def = context - .classes - .get(&constructor.class_name) - .or_else(|| context.classes.get(&normalized_class_name)) - .cloned(); + let class_def = context.classes.get(&constructor.class_name).cloned(); if let Some(class_def) = class_def { // Create a map of field names to types diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 6dbb619d86..9089f5fd27 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -81,16 +81,19 @@ impl WatchChannels { ) }); channels.extend(md_channels); - let var_channels = watch_vars.into_iter().map(|(_, (watch_spec, chan_type))| { - ( - ChannelFQN { - namespace: None, - r#type: ChannelType::Variable, - name: watch_spec.name.clone(), - }, - chan_type.clone(), - ) - }); + let var_channels = + watch_vars + .into_iter() + .map(|(channel_name, (_watch_spec, chan_type))| { + ( + ChannelFQN { + namespace: None, + r#type: ChannelType::Variable, + name: channel_name.clone(), + }, + chan_type.clone(), + ) + }); channels.extend(var_channels); let mut dependencies = transitive_closures[fn_name].clone(); @@ -114,16 +117,18 @@ impl WatchChannels { }); channels.extend(sub_md_channels); let sub_var_channels = - watch_vars.into_iter().map(|(_, (watch_spec, chan_type))| { - ( - ChannelFQN { - namespace: Some(subfunction.clone()), - r#type: ChannelType::Variable, - name: watch_spec.name.clone(), - }, - chan_type.clone(), - ) - }); + watch_vars + .into_iter() + .map(|(channel_name, (_watch_spec, chan_type))| { + ( + ChannelFQN { + namespace: Some(subfunction.clone()), + r#type: ChannelType::Variable, + name: channel_name.clone(), + }, + chan_type.clone(), + ) + }); channels.extend(sub_var_channels); } } @@ -224,7 +229,17 @@ impl FunctionMetadata { if let Some(spec) = watch { match &value.meta().1 { Some(var_type) => { - self.push_watch_var(spec.name.clone(), spec.clone(), var_type.clone()); + // Create a default channel with the variable's own name + self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + + // If the WatchSpec has a different configured name, create that channel too + if &spec.name != name { + self.push_watch_var( + spec.name.clone(), + spec.clone(), + var_type.clone(), + ); + } } None => { diagnostics.push_error(DatamodelError::new_validation_error( @@ -252,7 +267,17 @@ impl FunctionMetadata { if let Some(spec) = watch { match &value.meta().1 { Some(var_type) => { - self.push_watch_var(spec.name.clone(), spec.clone(), var_type.clone()); + // Create a default channel with the variable's own name + self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + + // If the WatchSpec has a different configured name, create that channel too + if &spec.name != name { + self.push_watch_var( + spec.name.clone(), + spec.clone(), + var_type.clone(), + ); + } } None => { diagnostics.push_error(DatamodelError::new_validation_error( @@ -293,62 +318,11 @@ impl FunctionMetadata { thir::Statement::Assert { condition, .. } => { self.analyze_expression(condition, diagnostics); } - }; - } - - /// Handle a call to VAR_NAME.$watch.options(baml.WatchOptions{...}) - /// Extract the channel name from the WatchOptions constructor and update the channel type. - fn handle_watch_options_call( - &mut self, - var_name: &str, - args: &[thir::Expr], - _meta: &ExprMetadata, - diagnostics: &mut Diagnostics, - ) { - // The argument should be a ClassConstructor for baml.WatchOptions - if args.len() != 1 { - return; // Type checker should have caught this - } - - if let thir::Expr::ClassConstructor { fields, .. } = &args[0] { - // Extract the "name" field value - for field in fields { - if let ClassConstructorField::Named { name, value } = field { - if name == "name" { - if let thir::Expr::Value(baml_value) = value { - // Extract string value from BamlValueWithMeta::String - if let baml_types::BamlValueWithMeta::String(channel_name, _) = - baml_value - { - // Find the watch variable and update its channel name - if let Some((spec, var_type)) = self.watch_vars.get(var_name) { - let new_spec = WatchSpec { - name: channel_name.clone(), - when: spec.when.clone(), - span: spec.span.clone(), - }; - let var_type_clone = var_type.clone(); - // The immutable borrow ends here naturally - self.push_watch_var( - var_name.to_string(), - new_spec, - var_type_clone, - ); - } else { - diagnostics.push_error(DatamodelError::new_validation_error( - &format!( - "Variable '{}' is not a watched variable", - var_name - ), - baml_value.meta().0.clone(), - )); - } - } - } - } - } + thir::Statement::WatchOptions { .. } | thir::Statement::WatchNotify { .. } => { + // These are runtime statements that update watch specs dynamically + // No static analysis needed here } - } + }; } /// Walk the parts of an expression, appending metadata. @@ -365,31 +339,7 @@ impl FunctionMetadata { thir::Expr::FieldAccess { base, .. } => { self.analyze_expression(base, diagnostics); } - thir::Expr::MethodCall { - receiver, - method, - args, - meta, - } => { - // Check for VAR_NAME.$watch.options(baml.WatchOptions{name: "...", ...}) - if let thir::Expr::FieldAccess { base, field, .. } = receiver.as_ref() { - if field == "$watch" { - if let thir::Expr::Var(method_name, _) = method.as_ref() { - if method_name == "options" { - // Extract the variable name and channel configuration - if let thir::Expr::Var(var_name, _) = base.as_ref() { - self.handle_watch_options_call( - var_name, - args, - meta, - diagnostics, - ); - } - } - } - } - } - + thir::Expr::MethodCall { receiver, args, .. } => { self.analyze_expression(receiver, diagnostics); for arg in args { self.analyze_expression(arg, diagnostics); @@ -680,19 +630,6 @@ mod tests { } } - #[test] - fn test_plain_let() { - let hir = Hir::from_source( - r#" - function A() -> int { - let a_1 = 1; - 1 - } - "#, - ); - assert_eq!(hir.expr_functions.len(), 1); - } - #[test] fn test_let_watch_simple() { let hir = Hir::from_source( @@ -707,7 +644,7 @@ mod tests { } #[test] - fn test_let_watch_with_options() { + fn test_watch_let_with_options() { let hir = Hir::from_source( r#" function A() -> int { @@ -723,6 +660,65 @@ mod tests { assert_eq!(hir.expr_functions.len(), 2); } + #[test] + fn test_watch_let_shared_channel() { + let hir = Hir::from_source( + r#" + function A() -> int { + watch let x = 1; + x.$watch.notify(); + x.$watch.options(baml.WatchOptions{ name: "c"}); + watch let y = 1; + y.$watch.options(baml.WatchOptions{ name: "c"}); + 0 + } + "#, + ); + let mut diagnostics = Diagnostics::new(PathBuf::from("test")); + let thir = typecheck(&hir, &mut diagnostics); + let watch_channels = WatchChannels::analyze_program(&thir, &mut diagnostics); + let a_channels = watch_channels.functions_channels.get("A").unwrap(); + + // Should have 3 channels: "x", "y", and "c" + assert_eq!(a_channels.channels.len(), 3); + + // Check that we have a channel named "x" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "x" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + + // Check that we have a channel named "y" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "y" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + + // Check that we have a channel named "c" (shared by both x and y) + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "c" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + } + #[test] #[ignore = "$watch special field not yet implemented"] fn test_field_access_statement() { diff --git a/engine/baml-compiler/tests/builtins.rs b/engine/baml-compiler/tests/builtins.rs index 161252470a..7f4dfde205 100644 --- a/engine/baml-compiler/tests/builtins.rs +++ b/engine/baml-compiler/tests/builtins.rs @@ -21,7 +21,7 @@ fn builtin_method_call() -> anyhow::Result<()> { Instruction::LoadConst(1), Instruction::LoadConst(2), Instruction::AllocArray(3), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(1), // call with one argument (self) Instruction::Call(1), @@ -49,7 +49,7 @@ fn fetch_as() -> anyhow::Result<()> { expected: vec![( "main", vec![ - Instruction::LoadGlobal(GlobalIndex::from_raw(38)), + Instruction::LoadGlobal(GlobalIndex::from_raw(39)), Instruction::LoadConst(0), Instruction::LoadConst(1), Instruction::DispatchFuture(2), diff --git a/engine/baml-compiler/tests/classes.rs b/engine/baml-compiler/tests/classes.rs index d3f33b4684..f2083e6937 100644 --- a/engine/baml-compiler/tests/classes.rs +++ b/engine/baml-compiler/tests/classes.rs @@ -22,7 +22,7 @@ fn class_constructor() -> anyhow::Result<()> { expected: vec![( "main", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(2)), + Instruction::AllocInstance(ObjectIndex::from_raw(3)), Instruction::Copy(0), Instruction::LoadConst(0), Instruction::StoreField(0), @@ -59,7 +59,7 @@ fn class_constructor_with_spread_operator() -> anyhow::Result<()> { expected: vec![( "main", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(3)), + Instruction::AllocInstance(ObjectIndex::from_raw(4)), Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), Instruction::Copy(1), @@ -109,7 +109,7 @@ fn class_constructor_with_spread_before_named_fields() -> anyhow::Result<()> { expected: vec![( "main", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(3)), + Instruction::AllocInstance(ObjectIndex::from_raw(4)), Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), Instruction::Copy(1), @@ -157,7 +157,7 @@ fn class_constructor_with_spread_after_named_fields() -> anyhow::Result<()> { expected: vec![( "main", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(3)), + Instruction::AllocInstance(ObjectIndex::from_raw(4)), Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), Instruction::Copy(1), @@ -217,7 +217,7 @@ fn class_constructor_with_multiple_spread_operators() -> anyhow::Result<()> { ( "xy_one_last", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(5)), + Instruction::AllocInstance(ObjectIndex::from_raw(6)), Instruction::LoadGlobal(GlobalIndex::from_raw(1)), Instruction::Call(0), Instruction::Copy(1), @@ -244,7 +244,7 @@ fn class_constructor_with_multiple_spread_operators() -> anyhow::Result<()> { ( "x_one_last", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(5)), + Instruction::AllocInstance(ObjectIndex::from_raw(6)), Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), Instruction::Copy(1), @@ -296,7 +296,7 @@ fn class_constructor_with_spread_operator_does_not_break_locals() -> anyhow::Res expected: vec![( "main", vec![ - Instruction::AllocInstance(ObjectIndex::from_raw(3)), + Instruction::AllocInstance(ObjectIndex::from_raw(4)), Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), Instruction::Copy(1), @@ -376,10 +376,10 @@ fn nested_field_read_bytecode() -> anyhow::Result<()> { "main", vec![ // Create Outer { inner: Inner { value: 42 } } - Instruction::AllocInstance(ObjectIndex::from_raw(3)), // Outer class + Instruction::AllocInstance(ObjectIndex::from_raw(4)), // Outer class Instruction::Copy(0), // Copy Outer instance // Create Inner inline - Instruction::AllocInstance(ObjectIndex::from_raw(2)), // Inner class + Instruction::AllocInstance(ObjectIndex::from_raw(3)), // Inner class Instruction::Copy(0), // Copy Inner instance Instruction::LoadConst(0), // 42 Instruction::StoreField(0), // Inner.value = 42 @@ -419,10 +419,10 @@ fn nested_object_construction_bytecode() -> anyhow::Result<()> { "main", vec![ // Outer constructor - Instruction::AllocInstance(ObjectIndex::from_raw(3)), // Outer + Instruction::AllocInstance(ObjectIndex::from_raw(4)), // Outer Instruction::Copy(0), // Copy Outer instance // Nested Inner construction - Instruction::AllocInstance(ObjectIndex::from_raw(2)), // Inner + Instruction::AllocInstance(ObjectIndex::from_raw(3)), // Inner Instruction::Copy(0), // Copy Inner instance Instruction::LoadConst(0), // 10 Instruction::StoreField(0), // x = 10 diff --git a/engine/baml-compiler/tests/control_flow.rs b/engine/baml-compiler/tests/control_flow.rs index bdf11972fb..be8d9add6e 100644 --- a/engine/baml-compiler/tests/control_flow.rs +++ b/engine/baml-compiler/tests/control_flow.rs @@ -867,7 +867,7 @@ fn for_loop_sum() -> anyhow::Result<()> { vec![ Instruction::LoadConst(0), Instruction::LoadVar(1), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(3), Instruction::Call(1), Instruction::LoadConst(0), @@ -920,7 +920,7 @@ fn for_with_break() -> anyhow::Result<()> { vec![ Instruction::LoadConst(0), Instruction::LoadVar(1), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(3), Instruction::Call(1), Instruction::LoadConst(0), @@ -982,7 +982,7 @@ fn for_with_continue() -> anyhow::Result<()> { vec![ Instruction::LoadConst(0), Instruction::LoadVar(1), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(3), Instruction::Call(1), Instruction::LoadConst(0), @@ -1044,7 +1044,7 @@ fn for_nested() -> anyhow::Result<()> { vec![ Instruction::LoadConst(0), Instruction::LoadVar(1), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(4), Instruction::Call(1), Instruction::LoadConst(0), @@ -1061,7 +1061,7 @@ fn for_nested() -> anyhow::Result<()> { Instruction::BinOp(BinOp::Add), Instruction::StoreVar(6), Instruction::LoadVar(2), - Instruction::LoadGlobal(GlobalIndex::from_raw(3)), + Instruction::LoadGlobal(GlobalIndex::from_raw(4)), Instruction::LoadVar(8), Instruction::Call(1), Instruction::LoadConst(0), diff --git a/engine/baml-compiler/tests/enums.rs b/engine/baml-compiler/tests/enums.rs index 230c2924e4..0975ca40de 100644 --- a/engine/baml-compiler/tests/enums.rs +++ b/engine/baml-compiler/tests/enums.rs @@ -23,7 +23,7 @@ fn return_enum_variant() -> anyhow::Result<()> { "main", vec![ Instruction::LoadConst(0), - Instruction::AllocVariant(ObjectIndex::from_raw(3)), + Instruction::AllocVariant(ObjectIndex::from_raw(4)), Instruction::Return, ], )], @@ -49,7 +49,7 @@ fn assign_enum_variant() -> anyhow::Result<()> { "main", vec![ Instruction::LoadConst(0), - Instruction::AllocVariant(ObjectIndex::from_raw(3)), + Instruction::AllocVariant(ObjectIndex::from_raw(4)), Instruction::LoadVar(1), Instruction::Return, ], diff --git a/engine/baml-compiler/tests/maps.rs b/engine/baml-compiler/tests/maps.rs index cbc21d423e..b3c4a6b924 100644 --- a/engine/baml-compiler/tests/maps.rs +++ b/engine/baml-compiler/tests/maps.rs @@ -112,7 +112,7 @@ fn contains() -> anyhow::Result<()> { vec![ Instruction::LoadGlobal(GlobalIndex::from_raw(0)), Instruction::Call(0), - Instruction::LoadGlobal(GlobalIndex::from_raw(6)), + Instruction::LoadGlobal(GlobalIndex::from_raw(7)), Instruction::LoadVar(1), Instruction::LoadConst(0), Instruction::Call(2), @@ -194,7 +194,7 @@ fn len() -> anyhow::Result<()> { Instruction::LoadConst(2), Instruction::LoadConst(3), Instruction::AllocMap(2), - Instruction::LoadGlobal(GlobalIndex::from_raw(4)), + Instruction::LoadGlobal(GlobalIndex::from_raw(5)), Instruction::LoadVar(1), Instruction::Call(1), Instruction::Return, diff --git a/engine/baml-compiler/tests/watch.rs b/engine/baml-compiler/tests/watch.rs index 75df82b20b..0fe4729b9b 100644 --- a/engine/baml-compiler/tests/watch.rs +++ b/engine/baml-compiler/tests/watch.rs @@ -10,7 +10,7 @@ fn watch_primitive() -> anyhow::Result<()> { assert_compiles(Program { source: " function primitive() -> int { - let value = 0 @watch; + watch let value = 0; value = 1; diff --git a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml index c29848af6f..1fa4c93a35 100644 --- a/engine/baml-lib/baml/tests/hir_files/array_and_call.baml +++ b/engine/baml-lib/baml/tests/hir_files/array_and_call.baml @@ -29,18 +29,3 @@ function ArrayAccess() -> int { // // arr[1] // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/assert.baml b/engine/baml-lib/baml/tests/hir_files/assert.baml index f563f8a552..a6b6cd3e3d 100644 --- a/engine/baml-lib/baml/tests/hir_files/assert.baml +++ b/engine/baml-lib/baml/tests/hir_files/assert.baml @@ -22,18 +22,3 @@ function assertNotOk() -> int { // // 2 // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/basic_class.baml b/engine/baml-lib/baml/tests/hir_files/basic_class.baml index 5bc3a5b0e7..4f9f162766 100644 --- a/engine/baml-lib/baml/tests/hir_files/basic_class.baml +++ b/engine/baml-lib/baml/tests/hir_files/basic_class.baml @@ -3,22 +3,7 @@ class Person { age int } -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// // class Person { // name: string // age: int // } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/classes.baml b/engine/baml-lib/baml/tests/hir_files/classes.baml index e1eb8445e0..c86bfc8c5c 100644 --- a/engine/baml-lib/baml/tests/hir_files/classes.baml +++ b/engine/baml-lib/baml/tests/hir_files/classes.baml @@ -18,22 +18,7 @@ function TestClass() -> int { // example.a // } // -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// // class Example { // a: int // b: string // } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/enum_example.baml b/engine/baml-lib/baml/tests/hir_files/enum_example.baml index 6aa580130a..0960689f15 100644 --- a/engine/baml-lib/baml/tests/hir_files/enum_example.baml +++ b/engine/baml-lib/baml/tests/hir_files/enum_example.baml @@ -9,26 +9,11 @@ class Widget { size int } -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// // class Widget { // color: Color // size: int // } // -// enum std.HttpMethod { -// Get -// } -// // enum Color { // Red // Green diff --git a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml index 95b68576d0..d4f4b06b3e 100644 --- a/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/expression_with_let.baml @@ -8,18 +8,3 @@ function AddOne(x: int) -> int { // // y // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml index 3a3afea1e5..57ce69282f 100644 --- a/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml +++ b/engine/baml-lib/baml/tests/hir_files/if_expression_let.baml @@ -14,18 +14,3 @@ function simpleIf() -> string { // // x // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/literal_values.baml b/engine/baml-lib/baml/tests/hir_files/literal_values.baml index b5b7980a40..0f76cf4e15 100644 --- a/engine/baml-lib/baml/tests/hir_files/literal_values.baml +++ b/engine/baml-lib/baml/tests/hir_files/literal_values.baml @@ -38,18 +38,3 @@ function ReturnArray() -> int[] { // function ReturnArray() { // [1, 2, 3, 4, 5] // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml index 283caf435b..0cf01928f4 100644 --- a/engine/baml-lib/baml/tests/hir_files/llm_functions.baml +++ b/engine/baml-lib/baml/tests/hir_files/llm_functions.baml @@ -60,21 +60,6 @@ function AnalyzeSentiment(text: string) -> Sentiment { // prompt "Analyze the sentiment of this text: {{ text }}\n\nRespond with exactly one of: POSITIVE, NEGATIVE, NEUTRAL" // } // -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } -// // enum Sentiment { // POSITIVE // NEGATIVE diff --git a/engine/baml-lib/baml/tests/hir_files/loops/break.baml b/engine/baml-lib/baml/tests/hir_files/loops/break.baml index 16d7e45092..710a0999c5 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/break.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/break.baml @@ -54,18 +54,3 @@ function Nested() -> int { // // a // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml index c62f22d038..d4fba08055 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/c_for.baml @@ -93,18 +93,3 @@ function Nothing() -> int { // // s // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml index 0a86ff0d37..9330ef2ee7 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/continue.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/continue.baml @@ -51,18 +51,3 @@ function ContinueNested() -> int { // } // } // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/for.baml b/engine/baml-lib/baml/tests/hir_files/loops/for.baml index f6b8f14f2e..15f761ff04 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/for.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/for.baml @@ -16,18 +16,3 @@ function Sum(xs: int[]) -> int { // // result // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml index 4cb40663fd..282fcd9875 100644 --- a/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml +++ b/engine/baml-lib/baml/tests/hir_files/loops/while_loops.baml @@ -22,18 +22,3 @@ function GCD(a: int, b: int) -> int { // // a // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/maps.baml b/engine/baml-lib/baml/tests/hir_files/maps.baml index 0173ff0518..63986ee59d 100644 --- a/engine/baml-lib/baml/tests/hir_files/maps.baml +++ b/engine/baml-lib/baml/tests/hir_files/maps.baml @@ -84,18 +84,3 @@ function Len() -> int { // // map.length() // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml index 9221f96e2e..e49d7f87f4 100644 --- a/engine/baml-lib/baml/tests/hir_files/mut_variables.baml +++ b/engine/baml-lib/baml/tests/hir_files/mut_variables.baml @@ -24,18 +24,3 @@ function MutableInArg(x: int) -> int { // // x // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml index 07f0527989..9220ce0ad4 100644 --- a/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml +++ b/engine/baml-lib/baml/tests/hir_files/nested_types_with_attributes.baml @@ -5,24 +5,9 @@ class DataModel { config map @stream.not_null } -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// // class DataModel { // tags: array // status: (string @stream.done | int @stream.done) @stream.done // matrix: array> // config: map @stream.needed // } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/operators.baml b/engine/baml-lib/baml/tests/hir_files/operators.baml index fed0111b0e..ad506dee2a 100644 --- a/engine/baml-lib/baml/tests/hir_files/operators.baml +++ b/engine/baml-lib/baml/tests/hir_files/operators.baml @@ -9,18 +9,3 @@ function nestedOps() -> int { // // x // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/return_statement.baml b/engine/baml-lib/baml/tests/hir_files/return_statement.baml index 85d1f4e8a8..de39df4e08 100644 --- a/engine/baml-lib/baml/tests/hir_files/return_statement.baml +++ b/engine/baml-lib/baml/tests/hir_files/return_statement.baml @@ -62,18 +62,3 @@ function WithStack(x: int) -> int { // // 7 // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml index 5f2ecfe5de..e6dc3f9a38 100644 --- a/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml +++ b/engine/baml-lib/baml/tests/hir_files/streaming_and_constraints.baml @@ -9,17 +9,6 @@ class User { age int @check(valid_age, {{ this >= 0 }}) } -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// // class MyClass { // field1: string @stream.done // field2: int @stream.needed @@ -30,7 +19,3 @@ class User { // name: string @constrained // age: int @constrained // } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-lib/baml/tests/hir_files/watch.baml b/engine/baml-lib/baml/tests/hir_files/watch.baml index c6c65b2d13..b2cdeb6750 100644 --- a/engine/baml-lib/baml/tests/hir_files/watch.baml +++ b/engine/baml-lib/baml/tests/hir_files/watch.baml @@ -21,18 +21,3 @@ function MyFunction(prev: int, next: int) -> bool { // function MyFunction(prev, next) { // true // } -// -// class std.Request { -// base_url: string -// headers: map -// query_params: map -// } -// -// class baml.WatchOptions { -// name: string -// when: ("never" | "manual" | (ANY, ANY) -> bool) -// } -// -// enum std.HttpMethod { -// Get -// } diff --git a/engine/baml-runtime/src/cli/dump_intermediate.rs b/engine/baml-runtime/src/cli/dump_intermediate.rs index 8933c9595f..7d8cef5286 100644 --- a/engine/baml-runtime/src/cli/dump_intermediate.rs +++ b/engine/baml-runtime/src/cli/dump_intermediate.rs @@ -91,8 +91,9 @@ impl DumpIntermediateArgs { } fn dump_hir(&self, validated_schema: &ValidatedSchema) -> Result<()> { - // Convert to HIR + // Convert to HIR (filtering is done in to_doc()) let hir = Hir::from_ast(&validated_schema.db.ast); + let mut w = Vec::new(); hir.to_doc() .render(78, &mut w) @@ -108,28 +109,40 @@ impl DumpIntermediateArgs { fn dump_bytecode(&self, validated_schema: &ValidatedSchema) -> Result<()> { let program = compile(&validated_schema.db)?; - // Create a map of function name to function for easy lookup - let functions: std::collections::HashMap<&str, &baml_vm::Function> = program - .objects - .iter() - .filter_map(|obj| match obj { - Object::Function(f) => Some((f.name.as_str(), f)), - _ => None, - }) - .collect(); - - for (name, function) in functions { - println!("{name}"); - println!( - "{}", - baml_vm::debug::display_bytecode( - function, - &EvalStack::new(), - &program.objects, - &program.globals, - true - ) - ); + // Filter and display objects, excluding builtins + for obj in &program.objects { + match obj { + Object::Function(f) => { + // Skip builtin functions (though we don't have any compiled as bytecode currently) + if baml_compiler::builtin::is_builtin_identifier(&f.name) { + continue; + } + println!("{}", f.name); + println!( + "{}", + baml_vm::debug::display_bytecode( + f, + &EvalStack::new(), + &program.objects, + &program.globals, + true + ) + ); + } + Object::Class(c) => { + if !baml_compiler::builtin::is_builtin_class(&c.name) { + println!("Class: {} with {} fields", c.name, c.field_names.len()); + } + } + Object::Enum(e) => { + if !baml_compiler::builtin::is_builtin_enum(&e.name) { + println!("Enum {}", e.name); + } + } + _ => { + // Skip other object types (Instance, etc.) + } + } } Ok(()) diff --git a/engine/baml-runtime/src/cli/repl.rs b/engine/baml-runtime/src/cli/repl.rs index 986da02c53..4cd1eee815 100644 --- a/engine/baml-runtime/src/cli/repl.rs +++ b/engine/baml-runtime/src/cli/repl.rs @@ -501,6 +501,16 @@ impl ReplState { let input_expr_thir = typecheck_expression(&input_expr_hir, &type_context, &mut type_diagnostics); + // Check for type errors in the user's expression + if type_diagnostics.has_errors() { + let error_messages: Vec = type_diagnostics + .errors() + .iter() + .map(|e| e.message().to_string()) + .collect(); + return Err(anyhow!("Type error: {}", error_messages.join("; "))); + } + // let variables: IndexMap>> = self let variables: IndexMap> = self diff --git a/engine/baml-vm/tests/builtins.rs b/engine/baml-vm/tests/builtins.rs index 2fc9501de9..543a53f3d5 100644 --- a/engine/baml-vm/tests/builtins.rs +++ b/engine/baml-vm/tests/builtins.rs @@ -16,7 +16,7 @@ fn builtin_method_call() -> anyhow::Result<()> { } "#, function: "main", - expected: ExecState::Complete(Value::Int(3)), + expected: ExecState::Complete(Value::Int(4)), }) } diff --git a/engine/baml-vm/tests/watch.rs b/engine/baml-vm/tests/watch.rs index 8c3a6408f5..4eff831855 100644 --- a/engine/baml-vm/tests/watch.rs +++ b/engine/baml-vm/tests/watch.rs @@ -8,7 +8,7 @@ fn notify_primitive_on_change() -> anyhow::Result<()> { assert_vm_emits(EmitProgram { source: r#" function primitive() -> int { - let value = 0 @watch; + watch let value = 0; value = 1; @@ -25,7 +25,7 @@ fn notify_primitive_on_nested_scope() -> anyhow::Result<()> { assert_vm_emits(EmitProgram { source: r#" function primitive() -> int { - let value = 0 @watch; + watch let value = 0; if (true) { value = 1; From 5dc9f76825131f4b4c4b87c363f1639f1694a997 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 20 Oct 2025 15:44:35 -0700 Subject: [PATCH 08/16] interpret new watch syntax --- engine/baml-compiler/src/hir/lowering.rs | 17 +----- engine/baml-compiler/src/thir/interpret.rs | 58 ++++++++++++++++--- engine/baml-compiler/src/watch.rs | 30 +++++++++- .../test-files/workflows/workflow_emit.baml | 17 +++--- .../workflows/workflow_emit_simple.baml | 6 +- integ-tests/go/baml_client/baml_source_map.go | 6 +- .../python-v1/baml_client/inlinedbaml.py | 6 +- integ-tests/python/baml_client/inlinedbaml.py | 6 +- integ-tests/react/baml_client/inlinedbaml.ts | 6 +- .../typescript-esm/baml_client/inlinedbaml.ts | 6 +- .../typescript/baml_client/inlinedbaml.ts | 6 +- 11 files changed, 111 insertions(+), 53 deletions(-) diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 46eea06e3f..cd5db97053 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -497,21 +497,8 @@ fn lower_stmt_with_options( let watch_spec = if *is_watched { let var_name = identifier.to_string(); - let mut spec = WatchSpec::default_for_variable(var_name.clone(), span.clone()); - - // Apply watch options if they exist for this variable - if let Some((name_opt, when_opt)) = watch_options.get(&var_name) { - if let Some(custom_name) = name_opt { - spec.name = custom_name.clone(); - } - if let Some(when_fn) = when_opt { - spec.when = crate::watch::WatchWhen::FunctionName(ast::Identifier::Local( - when_fn.clone(), - span.clone(), - )); - } - } - + // Create default watch spec - runtime WatchOptions statements will modify it + let spec = WatchSpec::default_for_variable(var_name.clone(), span.clone()); Some(spec) } else { None diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index b8dbb577e1..9796ae4a15 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -197,14 +197,6 @@ async fn check_watch_changes( let last_notified = watch_var.last_notified.lock().unwrap().clone(); let last_checked = watch_var.last_checked.lock().unwrap().clone(); - log::debug!( - "Collecting watch var '{}': current={:?}, last_notified={:?}, last_checked={:?}", - watch_var.name, - current_value, - last_notified, - last_checked - ); - checks.push(( watch_var.name.clone(), watch_var.spec.clone(), @@ -1247,6 +1239,56 @@ where _ => bail!("assert condition must be boolean"), } } + Statement::WatchOptions { + variable, + name, + when, + span, + } => { + // Find and update the watch variable for this variable + // We need to find the watch variable by checking which one references the same value + for scope in scopes.iter_mut().rev() { + if let Some(var_ref) = scope.variables.get(variable) { + // Find the watch variable that references this variable + if let Some(watch_var) = scope + .watch_variables + .iter_mut() + .find(|wv| Arc::ptr_eq(&wv.value_ref, var_ref)) + { + // Update the channel name if provided + if let Some(new_name) = name { + watch_var.spec.name = new_name.clone(); + } + + // Update the when condition if provided + if let Some(when_str) = when { + watch_var.spec.when = match when_str.as_str() { + "manual" => crate::watch::WatchWhen::Manual, + "true" => crate::watch::WatchWhen::True, + _ => crate::watch::WatchWhen::FunctionName( + internal_baml_ast::ast::Identifier::Local( + when_str.clone(), + span.clone(), + ), + ), + }; + } + + watch_var.spec.span = span.clone(); + break; + } + } + } + } + Statement::WatchNotify { variable, .. } => { + // Manually trigger a watch notification for this variable + fire_watch_notification_for_variable( + scopes, + variable, + watch_handler, + function_name, + )?; + } } } diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 9089f5fd27..863efca42e 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -318,9 +318,33 @@ impl FunctionMetadata { thir::Statement::Assert { condition, .. } => { self.analyze_expression(condition, diagnostics); } - thir::Statement::WatchOptions { .. } | thir::Statement::WatchNotify { .. } => { - // These are runtime statements that update watch specs dynamically - // No static analysis needed here + thir::Statement::WatchOptions { + variable, + name, + span, + .. + } => { + // If a new channel name is configured, we need to create that channel + if let Some(new_name) = name { + // Find the existing watch variable for this variable to get its type + if let Some((_, var_type)) = self.watch_vars.get(variable) { + // Create a channel for the new name if it doesn't already exist + if !self.watch_vars.contains_key(new_name) { + self.push_watch_var( + new_name.clone(), + crate::watch::WatchSpec { + name: new_name.clone(), + when: crate::watch::WatchWhen::True, + span: span.clone(), + }, + var_type.clone(), + ); + } + } + } + } + thir::Statement::WatchNotify { .. } => { + // Manual notification - no static analysis needed } }; } diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit.baml b/integ-tests/baml_src/test-files/workflows/workflow_emit.baml index d692fe39b0..4f694301c1 100644 --- a/integ-tests/baml_src/test-files/workflows/workflow_emit.baml +++ b/integ-tests/baml_src/test-files/workflows/workflow_emit.baml @@ -1,12 +1,12 @@ function WorkflowWatch() -> int { - let x: int = 10 @watch; - let y: bool = true @watch; - let once: string = "Hello" @watch; - let twice: string[] = ["Takedown", ""] @watch; - let story = LLMEcho(#" + watch let x: int = 10; + watch let y: bool = true; + watch let once: string = "Hello"; + watch let twice: string[] = ["Takedown", ""]; + watch let story = LLMEcho(#" Hello world this is a test of a longish string. I want it to be long enough to generate a few chunks. - "#) @watch; + "#); y = false; x = WorkflowWatchChild(); @@ -15,7 +15,7 @@ function WorkflowWatch() -> int { } function WorkflowWatchChild() -> int { - let x: string = "Hello" @watch; + watch let x: string = "Hello"; 100 } @@ -53,7 +53,8 @@ function WorkflowWatchWithFilter() -> int { "elderberry", "banana", "fig", "grape", "honeydew"]; // This variable will only notify watchers when the word is "banana" (as determined by LLM) - let this_word: string = "" @watch(when=IsTargetWord); + watch let this_word: string = ""; + this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord }) let i: int = 0; for (let word in words) { diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml index 8f13e8beca..97cf1c2c7b 100644 --- a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml +++ b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml @@ -5,13 +5,17 @@ function NotEmpty(prev: string, next: string) -> bool { // Simple test without loops function SimpleWatchWithFilter() -> int { - let word: string = "" @watch(when=NotEmpty); + watch let word: string = ""; + word.$watch.options(baml.WatchOptions{ when: NotEmpty }); word = "hello"; // Should notify (not empty) word = "world"; // Should notify (not empty) word = ""; // Should NOT notify (empty) word = "test"; // Should notify (not empty) + word.$watch.options(baml.WatchOptions{ name: "new_name" }); + word = "with_new_name"; // Should notify (not empty) + 42 } diff --git a/integ-tests/go/baml_client/baml_source_map.go b/integ-tests/go/baml_client/baml_source_map.go index 0a46f3b798..c0875169e0 100644 --- a/integ-tests/go/baml_client/baml_source_map.go +++ b/integ-tests/go/baml_client/baml_source_map.go @@ -31,7 +31,7 @@ var file_map = map[string]string{ "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -133,8 +133,8 @@ var file_map = map[string]string{ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } func getBamlFiles() map[string]string { diff --git a/integ-tests/python-v1/baml_client/inlinedbaml.py b/integ-tests/python-v1/baml_client/inlinedbaml.py index 2423f9ab76..ea144d1c50 100644 --- a/integ-tests/python-v1/baml_client/inlinedbaml.py +++ b/integ-tests/python-v1/baml_client/inlinedbaml.py @@ -28,7 +28,7 @@ "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python/baml_client/inlinedbaml.py b/integ-tests/python/baml_client/inlinedbaml.py index 2423f9ab76..ea144d1c50 100644 --- a/integ-tests/python/baml_client/inlinedbaml.py +++ b/integ-tests/python/baml_client/inlinedbaml.py @@ -28,7 +28,7 @@ "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/react/baml_client/inlinedbaml.ts b/integ-tests/react/baml_client/inlinedbaml.ts index 20b3ba5ef1..fbd3daaa4d 100644 --- a/integ-tests/react/baml_client/inlinedbaml.ts +++ b/integ-tests/react/baml_client/inlinedbaml.ts @@ -36,7 +36,7 @@ const fileMap = { "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts index 20b3ba5ef1..fbd3daaa4d 100644 --- a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts @@ -36,7 +36,7 @@ const fileMap = { "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript/baml_client/inlinedbaml.ts b/integ-tests/typescript/baml_client/inlinedbaml.ts index 20b3ba5ef1..fbd3daaa4d 100644 --- a/integ-tests/typescript/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript/baml_client/inlinedbaml.ts @@ -36,7 +36,7 @@ const fileMap = { "test-files/aliases/aliased-inputs.baml": "\nclass InputClass {\n key string @alias(\"color\")\n key2 string\n}\n\n\nclass InputClassNested {\n key string\n nested InputClass @alias(\"interesting-key\")\n}\n \n\nfunction AliasedInputClass(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {{input}}\n\n This is a test. What's the name of the first json key above? Remember, tell me the key, not value.\n \"#\n}\n \nfunction AliasedInputClass2(input: InputClass) -> string {\n client GPT35\n prompt #\"\n\n {# making sure we can still access the original key #}\n {%if input.key == \"tiger\"%}\n Repeat this value back to me, and nothing else: {{input.key}}\n {%endif%}\n \"#\n}\n \n function AliasedInputClassNested(input: InputClassNested) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n\n {{input}}\n\n This is a test. What's the name of the second json key above? Remember, tell me the key, not value.\n \"#\n }\n\n\nenum AliasedEnum {\n KEY_ONE @alias(\"tiger\")\n KEY_TWO\n}\n\nfunction AliasedInputEnum(input: AliasedEnum) -> string {\n client GPT4o\n prompt #\"\n {{ _.role(\"user\")}}\n\n\n Write out this word only in your response, in lowercase:\n ---\n {{input}}\n ---\n Answer:\n \"#\n}\n\n\nfunction AliasedInputList(input: AliasedEnum[]) -> string {\n client GPT35\n prompt #\"\n {{ _.role(\"user\")}}\n Given this array:\n ---\n {{input}}\n ---\n\n Return the first element in the array:\n \"#\n}\n\n", "test-files/aliases/classes.baml": "class TestClassAlias {\n key string @alias(\"key-dash\") @description(#\"\n This is a description for key\n af asdf\n \"#)\n key2 string @alias(\"key21\")\n key3 string @alias(\"key with space\")\n key4 string //unaliased\n key5 string @alias(\"key.with.punctuation/123\")\n}\n\nfunction FnTestClassAlias(input: string) -> TestClassAlias {\n client GPT35\n prompt #\"\n {{ctx.output_format}}\n \"#\n}\n\ntest FnTestClassAlias {\n functions [FnTestClassAlias]\n args {\n input \"example input\"\n }\n}\n", "test-files/aliases/enums.baml": "enum TestEnum {\n A @alias(\"k1\") @description(#\"\n User is angry\n \"#)\n B @alias(\"k22\") @description(#\"\n User is happy\n \"#)\n // tests whether k1 doesnt incorrectly get matched with k11\n C @alias(\"k11\") @description(#\"\n User is sad\n \"#)\n D @alias(\"k44\") @description(#\"\n User is confused\n \"#)\n E @description(#\"\n User is excited\n \"#)\n F @alias(\"k5\") // only alias\n \n G @alias(\"k6\") @description(#\"\n User is bored\n With a long description\n \"#)\n \n @@alias(\"Category\")\n}\n\nfunction FnTestAliasedEnumOutput(input: string) -> TestEnum {\n client GPT35\n prompt #\"\n Classify the user input into the following category\n \n {{ ctx.output_format }}\n\n {{ _.role('user') }}\n {{input}}\n\n {{ _.role('assistant') }}\n Category ID:\n \"#\n}\n\ntest FnTestAliasedEnumOutput {\n functions [FnTestAliasedEnumOutput]\n args {\n input \"mehhhhh\"\n }\n}", - "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std::fetch_value(std::Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", + "test-files/builtin/fetch.baml": " // class Todo {\n // id int\n // todo string\n // completed bool\n // userId int\n // }\n\n // function GetTodo() -> Todo {\n // Todo {\n // id: 1,\n // }\n // }\n\n // function LlmDescribeTodo(todo: Todo) -> string {\n // client GPT4o\n // prompt #\"Describe the following todo in detail: {{ todo }}\"#\n // }\n\n // function UseGetTodoFunction() -> string {\n // let todo = GetTodo();\n // LlmDescribeTodo(todo)\n // }\n\n // test UseGetTodoFunction() {\n // functions [UseGetTodoFunction]\n // args { }\n // }\n\n // function SearchWikipedia(query: string) -> string {\n // std.fetch_value(std.Request {\n // base_url: UrlEncode(\"https://en.wikipedia.org/wiki/Special:Search\", {search query})\n // })\n // }\n\n // function UrlEncode(base_url: string, query_params: map) -> string {\n // client \"openai/o3\"\n // prompt #\"\n // Encode the following base URL and query parameters into a valid URL:\n\n // URL: {{ base_url }}\n\n // Query Params: {{ query_params }}\n\n // Answer with just the final URL, no other text\n // \"#\n // }\n\n // function ConvertUserQuery(query: string) -> string {\n // client GPT4o\n // prompt #\"\n // Convert the following user query into a search query for Wikipedia: {{ query }}\n\n // Ideally it should be a single word that could map to an actual Wikipedia page\n // \"#\n // }\n\n // function LlmFormulateResponse(query: string, search_result: string?) -> string {\n // client GPT4o\n // prompt #\"\n // You are a helpful assistant. Answer the following user query: {{ query }}.\n\n // {% if search_result != \"\" %}\n // Use this Wikipedia search result as a reference for your answer: {{ search_result }}\n // {% endif %}\n // \"#\n // }\n\n // function NeedsWikipediaSearch(query: string) -> bool {\n // client GPT4o\n // prompt #\"Does the following user query need a Wikipedia search?: {{ query }}\"#\n // }\n\n // function ChatResponse(query: string) -> string {\n // let needs_search = NeedsWikipediaSearch(query);\n // let search_result = SearchWikipedia(ConvertUserQuery(query));\n // LlmFormulateResponse(query, search_result)\n // }\n\n // test TestName {\n // functions [ChatResponse]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // messages []\n // }\n // }\n\n // test UrlEncode {\n // functions [UrlEncode]\n // args {\n // base_url \"https://en.wikipedia.org/wiki/Special:Search\"\n // query_params {search \"Tell me everything about Rome\"\n // go \"Go\"}\n // }\n // }\n\n // test Convert {\n // functions [ConvertUserQuery]\n // args {\n // query #\"\n // Tell me everything about Rome\n // \"#\n // }\n // }\n", "test-files/client-timeout-config.baml": "// Client with extremely short timeouts for testing\nclient TestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n connect_timeout_ms 1 // 1ms - will timeout on connect\n request_timeout_ms 10 // 10ms - very likely to timeout\n }\n }\n}\n\n// Function that will timeout\nfunction TestTimeoutError(input: string) -> string {\n client TestTimeoutClient\n prompt #\"\n This is a test that should timeout.\n Please write a very long response about: {{input}}\n \"#\n}\n\n// Client with only request timeout\nclient TestRequestTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 50 // 50ms timeout\n }\n }\n}\n\nfunction TestRequestTimeout(input: string) -> string {\n client TestRequestTimeoutClient\n prompt #\"\n Generate a detailed 500 word essay about: {{input}}\n \"#\n}\n\n// Client with zero timeout (infinite)\nclient TestZeroTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n request_timeout_ms 0 // 0 means infinite\n }\n }\n}\n\nfunction TestZeroTimeout(input: string) -> string {\n client TestZeroTimeoutClient\n prompt #\"\n Echo: {{input}}\n \"#\n}\n\n// Fallback client that includes timeout client\nclient TestTimeoutFallbackClient {\n provider fallback\n options {\n strategy [TestTimeoutClient, TestZeroTimeoutClient]\n }\n}\n\nfunction TestTimeoutFallback(input: string) -> string {\n client TestTimeoutFallbackClient\n prompt #\"\n Simple echo: {{input}}\n \"#\n}\n\n// Client for streaming timeout tests (Phase 4)\nclient TestStreamingTimeoutClient {\n provider openai\n options {\n model \"gpt-4o-mini\"\n api_key env.OPENAI_API_KEY\n http {\n time_to_first_token_timeout_ms 1 // 1ms - extremely short, should always timeout\n idle_timeout_ms 1 // 1ms - extremely short, should always timeout\n }\n }\n}\n\nfunction TestStreamingTimeout(input: string) -> string {\n client TestStreamingTimeoutClient\n prompt #\"\n Stream a long response about: {{input}}\n \"#\n}\n", "test-files/comments/comments.baml": "// add some functions, classes, enums etc with comments all over.", "test-files/constraints/constraints.baml": "// These classes and functions test several properties of\n// constrains:\n//\n// - The ability for constrains on fields to pass or fail.\n// - The ability for constraints on bare args and return types to pass or fail.\n// - The ability of constraints to influence which variant of a union is chosen\n// by the parser, when the structure is not sufficient to decide.\n\n/// A Martian organism with an age.\n/// Such a nice type.\nclass Martian {\n /// The age of the Martian in Mars years.\n /// So many Mars years.\n age int @check(young_enough, {{ this < 30 }})\n}\n\nclass Earthling {\n age int @check(earth_aged, {{this < 200 and this > 0}}) @check(no_infants, {{this >1}})\n}\n\n\nclass FooAny {\n planetary_age Martian | Earthling\n certainty int @check(unreasonably_certain, {{this == 102931}})\n species string @check(trivial, {{this == \"Homo sapiens\"}}) @check(regex_good, {{this|regex_match(\"Homo\")}}) @check(regex_bad, {{this|regex_match(\"neanderthalensis\")}})\n}\n\n\nfunction PredictAge(name: string) -> FooAny {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling\n and capitalization). I'll give you a hint: If the name\n is \"Greg\", his age is 41.\n\n {{ctx.output_format}}\n \"#\n}\n\n\nfunction PredictAgeBare(inp: string @assert(big_enough, {{this|length > 1}})) -> int @check(too_big, {{this == 10102}}) {\n client GPT35\n prompt #\"\n Using your understanding of the historical popularity\n of names, predict the age of a person with the name\n {{ inp.name }} in years. Also predict their genus and\n species. It's Homo sapiens (with exactly that spelling).\n\n {{ctx.output_format}}\n \"#\n}\n\nfunction ReturnFailingAssert(inp: int @assert(small_int, {{this < 10}})) -> int @assert(big_int, {{this > 100}}) {\n client GPT35\n prompt #\"\n Return the next integer after {{ inp }}.\n\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitle {\n title string\n story_a string @assert(too_long_story, {{this|length > 1000000}} )\n story_b string @assert(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingAssertion(theme: string, length: int) -> TwoStoriesOneTitle {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass TwoStoriesOneTitleCheck {\n title string\n story_a string @check(too_long_story, {{this|length > 1000000}} )\n story_b string @check(too_long_story, {{this|length > 1000000}} )\n}\n\nfunction StreamFailingCheck(theme: string, length: int) -> TwoStoriesOneTitleCheck {\n client GPT35\n prompt #\"\n Tell me two different stories along the theme of {{ theme }} with the same title.\n Please make each about {{ length }} words long.\n {{ctx.output_format}}\n \"#\n}\n\nclass BlockConstraint {\n foo int\n bar string\n @@check(cross_field, {{ this.bar|length > this.foo }})\n}\n\nfunction MakeBlockConstraint() -> BlockConstraint {\n client GPT35\n prompt #\"\n Generate an output in the following schema with a short string and a large int.\n\n {{ ctx.output_format }}\n \"#\n}\n\nclass NestedBlockConstraint {\n nbc BlockConstraint\n}\n\nclass BlockConstraintForParam {\n bcfp int\n bcfp2 string\n @@assert(hi, {{ this.bcfp2|length < this.bcfp }})\n}\n\nclass NestedBlockConstraintForParam {\n nbcfp BlockConstraintForParam\n}\n\nfunction MakeNestedBlockConstraint() -> NestedBlockConstraint {\n client GPT35\n prompt #\"Generate an output where the inner foo is 1 and the inner bar is \"hello\".\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseBlockConstraint(inp: BlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n\nfunction UseNestedBlockConstraint(inp: NestedBlockConstraintForParam) -> int {\n client GPT35\n prompt #\"\n Generate 3\n {{ ctx.output_format }}\n \"#\n}\n", @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n let x: int = 10 @watch;\n let y: bool = true @watch;\n let once: string = \"Hello\" @watch;\n let twice: string[] = [\"Takedown\", \"\"] @watch;\n let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#) @watch;\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n let x: string = \"Hello\" @watch;\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n let this_word: string = \"\" @watch(when=IsTargetWord);\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n let word: string = \"\" @watch(when=NotEmpty);\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; From bb065ad2c1025ccdeb39519ffc7255abf3028f96 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 12:24:58 -0700 Subject: [PATCH 09/16] rename WatchOptions::name to channel --- engine/baml-compiler/src/hir.rs | 4 +- engine/baml-compiler/src/hir/dump.rs | 4 +- engine/baml-compiler/src/hir/lowering.rs | 6 +-- engine/baml-compiler/src/thir.rs | 8 ++-- engine/baml-compiler/src/thir/interpret.rs | 20 +++++++--- engine/baml-compiler/src/thir/typecheck.rs | 4 +- engine/baml-compiler/src/watch.rs | 12 +++--- engine/baml-compiler/src/watch/watch_event.rs | 37 +++++++++++++++---- engine/baml-runtime/src/async_vm_runtime.rs | 3 +- .../workflows/workflow_emit_simple.baml | 3 +- 10 files changed, 67 insertions(+), 34 deletions(-) diff --git a/engine/baml-compiler/src/hir.rs b/engine/baml-compiler/src/hir.rs index fecc58a41a..683c7e23a5 100644 --- a/engine/baml-compiler/src/hir.rs +++ b/engine/baml-compiler/src/hir.rs @@ -188,10 +188,10 @@ pub enum Statement { }, /// Configure watch options for a watched variable. - /// Syntax: `variable.$watch.options(name: "channel", when: FilterFunc)` + /// Syntax: `variable.$watch.options( baml.WatchOptions { channel: "channel", when: FilterFunc } )` WatchOptions { variable: String, - name: Option, + channel: Option, when: Option, span: Span, }, diff --git a/engine/baml-compiler/src/hir/dump.rs b/engine/baml-compiler/src/hir/dump.rs index 40f40b568e..ad5881ffbb 100644 --- a/engine/baml-compiler/src/hir/dump.rs +++ b/engine/baml-compiler/src/hir/dump.rs @@ -276,14 +276,14 @@ impl Statement { } Statement::WatchOptions { variable, - name, + channel, when, .. } => { let mut doc = RcDoc::text(variable.clone()).append(RcDoc::text(".$watch.options(")); let mut parts = vec![]; - if let Some(n) = name { + if let Some(n) = channel { parts.push( RcDoc::text("name: \"") .append(RcDoc::text(n.clone())) diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index cd5db97053..cc4eef5578 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -315,7 +315,7 @@ fn extract_watch_options_fields(expr: &ast::Expression) -> (Option, Opti for field in &class_ctor.fields { if let ast::ClassConstructorField::Named(field_name, field_value) = field { match field_name.name() { - "name" => { + "channel" => { // name should be a string value if let Expression::StringValue(s, _) = field_value { name = Some(s.clone()); @@ -563,10 +563,10 @@ fn lower_stmt_with_options( span, }) => { // Extract name and when from the WatchOptions expression - let (name, when) = extract_watch_options_fields(options_expr); + let (channel, when) = extract_watch_options_fields(options_expr); Statement::WatchOptions { variable: variable.to_string(), - name, + channel, when, span: span.clone(), } diff --git a/engine/baml-compiler/src/thir.rs b/engine/baml-compiler/src/thir.rs index 29444285ed..926a67a9ee 100644 --- a/engine/baml-compiler/src/thir.rs +++ b/engine/baml-compiler/src/thir.rs @@ -710,7 +710,7 @@ pub enum Statement { /// Configure watch options for a watched variable. WatchOptions { variable: String, - name: Option, + channel: Option, when: Option, span: Span, }, @@ -814,13 +814,13 @@ impl Statement { } Statement::WatchOptions { variable, - name, + channel, when, .. } => { let mut parts = vec![]; - if let Some(n) = name { - parts.push(format!("name: \"{}\"", n)); + if let Some(c) = channel { + parts.push(format!("channel: \"{}\"", c)); } if let Some(w) = when { parts.push(format!("when: {}", w)); diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index 9796ae4a15..2da909c6c1 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -133,10 +133,19 @@ fn fire_watch_notification_for_variable( // Find the variable in scopes for scope in scopes.iter().rev() { if let Some(value_ref) = scope.variables.get(var_name) { + // Find the watch variable to get the current channel name + let channel_name = scope + .watch_variables + .iter() + .find(|wv| Arc::ptr_eq(&wv.value_ref, value_ref)) + .map(|wv| wv.spec.name.clone()) + .unwrap_or_else(|| var_name.to_string()); + let current_value = value_ref.lock().unwrap(); let watch_value = expr_value_to_watch_value(current_value.clone()); let notification = crate::watch::WatchNotification::new_var( - var_name.to_string(), + var_name.to_string(), // variable name + channel_name, // current channel name from WatchSpec watch_value, function_name.to_string(), ); @@ -289,7 +298,8 @@ async fn check_watch_changes( // Fire the notification let watch_value = expr_value_to_watch_value(current_value); let notification = crate::watch::WatchNotification::new_var( - spec.name.clone(), + var_name.clone(), // variable name + spec.name.clone(), // channel name watch_value, function_name.to_string(), ); @@ -1241,7 +1251,7 @@ where } Statement::WatchOptions { variable, - name, + channel, when, span, } => { @@ -1256,8 +1266,8 @@ where .find(|wv| Arc::ptr_eq(&wv.value_ref, var_ref)) { // Update the channel name if provided - if let Some(new_name) = name { - watch_var.spec.name = new_name.clone(); + if let Some(new_channel) = channel { + watch_var.spec.name = new_channel.clone(); } // Update the when condition if provided diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 4c42975e5c..f01e159bca 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1171,7 +1171,7 @@ fn typecheck_statement( } hir::Statement::WatchOptions { variable, - name, + channel, when, span, } => { @@ -1188,7 +1188,7 @@ fn typecheck_statement( Some(thir::Statement::WatchOptions { variable: variable.clone(), - name: name.clone(), + channel: channel.clone(), when: when.clone(), span: span.clone(), }) diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 863efca42e..c54d007209 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -320,20 +320,20 @@ impl FunctionMetadata { } thir::Statement::WatchOptions { variable, - name, + channel, span, .. } => { // If a new channel name is configured, we need to create that channel - if let Some(new_name) = name { + if let Some(new_channel) = channel { // Find the existing watch variable for this variable to get its type if let Some((_, var_type)) = self.watch_vars.get(variable) { - // Create a channel for the new name if it doesn't already exist - if !self.watch_vars.contains_key(new_name) { + // Create a channel for the new channel name if it doesn't already exist + if !self.watch_vars.contains_key(new_channel) { self.push_watch_var( - new_name.clone(), + new_channel.clone(), crate::watch::WatchSpec { - name: new_name.clone(), + name: new_channel.clone(), when: crate::watch::WatchWhen::True, span: span.clone(), }, diff --git a/engine/baml-compiler/src/watch/watch_event.rs b/engine/baml-compiler/src/watch/watch_event.rs index 7b069807cb..5386a12dab 100644 --- a/engine/baml-compiler/src/watch/watch_event.rs +++ b/engine/baml-compiler/src/watch/watch_event.rs @@ -28,6 +28,7 @@ pub struct WatchValueMetadata { pub struct WatchNotification { pub value: WatchBamlValue, pub variable_name: Option, + pub channel_name: Option, pub function_name: String, pub is_stream: bool, } @@ -35,13 +36,26 @@ pub struct WatchNotification { impl fmt::Display for WatchNotification { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.value { - WatchBamlValue::Value(value) => { - if let Some(var_name) = &self.variable_name { + WatchBamlValue::Value(value) => match (&self.variable_name, &self.channel_name) { + (Some(var_name), Some(chan_name)) if var_name != chan_name => { + write!( + f, + "(var) {} [channel: {}]: {}", + var_name, + chan_name, + value.clone().value() + ) + } + (Some(var_name), _) => { write!(f, "(var) {}: {}", var_name, value.clone().value()) - } else { + } + (None, Some(chan_name)) => { + write!(f, "(channel) {}: {}", chan_name, value.clone().value()) + } + _ => { write!(f, "{}", value.clone().value()) } - } + }, WatchBamlValue::Block(label) => { write!(f, "(block) {label}") } @@ -66,12 +80,14 @@ impl fmt::Display for WatchNotification { impl WatchNotification { pub fn new_var( variable_name: String, + channel_name: String, value: BamlValueWithMeta, function_name: String, ) -> Self { Self { value: WatchBamlValue::Value(value), variable_name: Some(variable_name), + channel_name: Some(channel_name), function_name, is_stream: false, } @@ -84,7 +100,8 @@ impl WatchNotification { ) -> Self { Self { value: WatchBamlValue::Value(value), - variable_name: Some(variable_name), + variable_name: Some(variable_name.clone()), + channel_name: Some(variable_name), function_name, is_stream: true, } @@ -94,6 +111,7 @@ impl WatchNotification { Self { value: WatchBamlValue::Block(block_label), variable_name: None, + channel_name: None, function_name, is_stream: false, } @@ -106,7 +124,8 @@ impl WatchNotification { ) -> Self { Self { value: WatchBamlValue::StreamStart(stream_id), - variable_name: Some(variable_name), + variable_name: Some(variable_name.clone()), + channel_name: Some(variable_name), function_name, is_stream: true, } @@ -120,7 +139,8 @@ impl WatchNotification { ) -> Self { Self { value: WatchBamlValue::StreamUpdate(stream_id, value), - variable_name: Some(variable_name), + variable_name: Some(variable_name.clone()), + channel_name: Some(variable_name), function_name, is_stream: true, } @@ -133,7 +153,8 @@ impl WatchNotification { ) -> Self { Self { value: WatchBamlValue::StreamEnd(stream_id), - variable_name: Some(variable_name), + variable_name: Some(variable_name.clone()), + channel_name: Some(variable_name), function_name, is_stream: true, } diff --git a/engine/baml-runtime/src/async_vm_runtime.rs b/engine/baml-runtime/src/async_vm_runtime.rs index bd0c9c10ed..e79cbe8ead 100644 --- a/engine/baml-runtime/src/async_vm_runtime.rs +++ b/engine/baml-runtime/src/async_vm_runtime.rs @@ -369,7 +369,8 @@ impl BamlAsyncVmRuntime { BamlValueWithMeta::with_const_meta(¤t_value, fake_meta); let notification = watch::WatchNotification::new_var( - state.channel.to_owned(), + watched_var_name.to_owned(), // variable name + state.channel.to_owned(), // channel name baml_value_with_meta, function_name.to_owned(), ); diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml index 97cf1c2c7b..3e5a479368 100644 --- a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml +++ b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml @@ -13,8 +13,9 @@ function SimpleWatchWithFilter() -> int { word = ""; // Should NOT notify (empty) word = "test"; // Should notify (not empty) - word.$watch.options(baml.WatchOptions{ name: "new_name" }); + word.$watch.options(baml.WatchOptions{ channel: "new_name" }); word = "with_new_name"; // Should notify (not empty) + word.$watch.notify(); 42 } From 0ead9e5e5ac62ead3de26f266ef8ff302a018b1d Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 14:19:09 -0700 Subject: [PATCH 10/16] update when function type --- engine/baml-compiler/src/thir/interpret.rs | 42 +++++---------- engine/baml-compiler/src/thir/typecheck.rs | 28 ++++++++-- .../baml-lib/baml/tests/hir_files/watch.baml | 16 +++++- .../expr/emit_missing_value.baml | 12 ----- .../{emit_nested.baml => watch_nested.baml} | 0 .../validation_files/expr/watch_when.baml | 34 ++++++++++++ .../test-files/workflows/workflow_emit.baml | 8 +-- .../workflows/workflow_emit_simple.baml | 4 +- integ-tests/go/baml_client/baml_source_map.go | 4 +- .../python-v1/baml_client/async_client.py | 54 +++++++++---------- .../python-v1/baml_client/inlinedbaml.py | 4 +- .../python-v1/baml_client/sync_client.py | 54 +++++++++---------- integ-tests/python-v1/baml_client/watchers.py | 17 +++++- .../python/baml_client/async_client.py | 54 +++++++++---------- integ-tests/python/baml_client/inlinedbaml.py | 4 +- integ-tests/python/baml_client/sync_client.py | 54 +++++++++---------- integ-tests/python/baml_client/watchers.py | 17 +++++- integ-tests/react/baml_client/async_client.ts | 30 +++++------ .../react/baml_client/async_request.ts | 24 ++++----- integ-tests/react/baml_client/inlinedbaml.ts | 4 +- integ-tests/react/baml_client/react/hooks.tsx | 10 ++-- integ-tests/react/baml_client/react/server.ts | 24 ++++----- .../baml_client/react/server_streaming.ts | 24 ++++----- integ-tests/react/baml_client/sync_client.ts | 12 ++--- integ-tests/react/baml_client/sync_request.ts | 24 ++++----- integ-tests/react/baml_client/watchers.ts | 13 +++++ .../baml_client/async_client.ts | 30 +++++------ .../baml_client/async_request.ts | 24 ++++----- .../typescript-esm/baml_client/inlinedbaml.ts | 4 +- .../typescript-esm/baml_client/sync_client.ts | 12 ++--- .../baml_client/sync_request.ts | 24 ++++----- .../typescript-esm/baml_client/watchers.ts | 13 +++++ .../typescript/baml_client/async_client.ts | 30 +++++------ .../typescript/baml_client/async_request.ts | 24 ++++----- .../typescript/baml_client/inlinedbaml.ts | 4 +- .../typescript/baml_client/sync_client.ts | 12 ++--- .../typescript/baml_client/sync_request.ts | 24 ++++----- .../typescript/baml_client/watchers.ts | 13 +++++ 38 files changed, 435 insertions(+), 350 deletions(-) delete mode 100644 engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml rename engine/baml-lib/baml/tests/validation_files/expr/{emit_nested.baml => watch_nested.baml} (100%) create mode 100644 engine/baml-lib/baml/tests/validation_files/expr/watch_when.baml diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index 2da909c6c1..0ea99d8dc5 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -257,11 +257,10 @@ async fn check_watch_changes( // For filter functions, ALWAYS call the filter - it subsumes change detection // Evaluate the filter function log::debug!( - "Evaluating filter function '{fn_name}' for variable '{var_name}': prev={last_notified:?}, next={current_baml_value:?}" + "Evaluating filter function '{fn_name}' for variable '{var_name}': current={current_baml_value:?}" ); match evaluate_filter_function( fn_name, - last_notified.as_ref(), ¤t_baml_value, scopes, thir, @@ -309,11 +308,10 @@ async fn check_watch_changes( } /// Evaluate a filter function for watch -/// The filter function takes (prev_value, next_value) -> bool +/// The filter function takes (current_value) -> bool async fn evaluate_filter_function( fn_name: &internal_baml_ast::ast::Identifier, - prev_value: Option<&BamlValue>, - next_value: &BamlValue, + current_value: &BamlValue, scopes: &mut Vec, thir: &THir, run_llm_function: &mut F, @@ -331,40 +329,24 @@ where .with_context(|| format!("Filter function '{fn_name}' not found"))?; // Check arity - if filter_func.parameters.len() != 2 { + if filter_func.parameters.len() != 1 { bail!( - "Filter function '{}' must take exactly 2 parameters (prev, next)", + "Filter function '{}' must take exactly 1 parameter (current value)", fn_name ); } // Convert BamlValue to BamlValueWithMeta - let prev_with_meta = match prev_value { - Some(v) => { - log::debug!("Filter function prev_value (Some): {v:?}"); - baml_value_to_value_with_meta(v.clone()) - } - None => { - log::debug!("Filter function prev_value: None"); - BamlValueWithMeta::Null((Span::fake(), None)) - } - }; - log::debug!("Filter function next_value: {next_value:?}"); - let next_with_meta = baml_value_to_value_with_meta(next_value.clone()); + log::debug!("Filter function current_value: {current_value:?}"); + let value_with_meta = baml_value_to_value_with_meta(current_value.clone()); - // Create a new scope with the function parameters + // Create a new scope with the function parameter // Mark this as a filter context to prevent infinite recursion scopes.push(Scope { - variables: [ - ( - filter_func.parameters[0].name.clone(), - Arc::new(Mutex::new(prev_with_meta)), - ), - ( - filter_func.parameters[1].name.clone(), - Arc::new(Mutex::new(next_with_meta)), - ), - ] + variables: [( + filter_func.parameters[0].name.clone(), + Arc::new(Mutex::new(value_with_meta)), + )] .into_iter() .collect(), watch_variables: Vec::new(), diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index f01e159bca..20aef6b8b4 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1183,8 +1183,28 @@ fn typecheck_statement( )); } - // TODO: Validate that 'when' function exists and has correct signature - // For now, we just pass it through + // Validate the 'when' function if provided + if let Some(when_str) = when { + // Parse the when string to get the function name + // For now, when is just a string with the function name + let fn_name = + internal_baml_ast::ast::Identifier::Local(when_str.clone(), span.clone()); + + // Get the variable's type for validation (clone to avoid borrow issues) + let var_type = context.vars.get(variable).map(|vi| vi.ty.clone()); + + if let Some(var_type) = var_type { + // Create a WatchSpec to validate + let watch_spec = crate::watch::WatchSpec { + name: variable.clone(), + when: crate::watch::WatchWhen::FunctionName(fn_name), + span: span.clone(), + }; + + // Use the existing validation function + typecheck_emit(&watch_spec, &var_type, context, diagnostics); + } + } Some(thir::Statement::WatchOptions { variable: variable.clone(), @@ -2604,7 +2624,7 @@ fn typecheck_emit( WatchWhen::FunctionName(fn_name) => { let required_predicate_type = TypeIR::Arrow( Box::new(ArrowGeneric { - param_types: vec![var_type.clone(), var_type.clone()], + param_types: vec![var_type.clone()], return_type: TypeIR::bool(), }), Default::default(), @@ -2619,7 +2639,7 @@ fn typecheck_emit( Some(function_type) => { if !function_type.is_subtype(&required_predicate_type) { diagnostics.push_error(DatamodelError::new_validation_error( - &format!("Function '{fn_name}' has incorrect type"), + &format!("Function '{fn_name}' has incorrect type. Expected (T) -> bool, where T matches the variable type"), fn_name.span().clone(), )); } diff --git a/engine/baml-lib/baml/tests/hir_files/watch.baml b/engine/baml-lib/baml/tests/hir_files/watch.baml index b2cdeb6750..5705719930 100644 --- a/engine/baml-lib/baml/tests/hir_files/watch.baml +++ b/engine/baml-lib/baml/tests/hir_files/watch.baml @@ -2,11 +2,17 @@ function Foo() -> int { watch let x = 5; watch let y = 10; watch let z: int = 20; + z.$watch.options(baml.WatchOptions { when: MyFunctionGood }); + z.$watch.options(baml.WatchOptions { when: MyFunctionBad }); 10 } -function MyFunction(prev: int, next: int) -> bool { +function MyFunctionBad(prev: int, next: int) -> bool { + true +} + +function MyFunctionGood(prev: int, next: int) -> bool { true } @@ -14,10 +20,16 @@ function MyFunction(prev: int, next: int) -> bool { // let x = 5 @watch(name=x); // let y = 10 @watch(name=y); // let z: int = 20 @watch(name=z); +// z.$watch.options(when: MyFunctionGood); +// z.$watch.options(when: MyFunctionBad); // // 10 // } // -// function MyFunction(prev, next) { +// function MyFunctionBad(prev, next) { +// true +// } +// +// function MyFunctionGood(prev, next) { // true // } diff --git a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml b/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml deleted file mode 100644 index 1352368d02..0000000000 --- a/engine/baml-lib/baml/tests/validation_files/expr/emit_missing_value.baml +++ /dev/null @@ -1,12 +0,0 @@ -function WatchValid() -> int { - watch progress: int = 0; - progress.$watch.notify(); - progress -} - -// error: Error validating: Can only access fields on class instances -// --> expr/emit_missing_value.baml:3 -// | -// 2 | watch progress: int = 0; -// 3 | progress.$watch.notify(); -// | diff --git a/engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml b/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml similarity index 100% rename from engine/baml-lib/baml/tests/validation_files/expr/emit_nested.baml rename to engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml diff --git a/engine/baml-lib/baml/tests/validation_files/expr/watch_when.baml b/engine/baml-lib/baml/tests/validation_files/expr/watch_when.baml new file mode 100644 index 0000000000..c5bc82b0f7 --- /dev/null +++ b/engine/baml-lib/baml/tests/validation_files/expr/watch_when.baml @@ -0,0 +1,34 @@ +function Top() -> int { + watch let x = 10; + x.$watch.options(baml.WatchOptions{ when: GoodFunction }); + x.$watch.options(baml.WatchOptions{ when: BadFunction1 }); + x.$watch.options(baml.WatchOptions{ when: BadFunction2 }); + 1 +} + +function GoodFunction(i: int) -> bool { + true +} + +function BadFunction1(i: int, j: int) -> bool { + true +} + +function BadFunction2(i: string) -> bool { + true +} + +// error: Error validating: Function 'BadFunction1' has incorrect type. Expected (T) -> bool, where T matches the variable type +// --> expr/watch_when.baml:4 +// | +// 3 | x.$watch.options(baml.WatchOptions{ when: GoodFunction }); +// 4 | x.$watch.options(baml.WatchOptions{ when: BadFunction1 }); +// 5 | x.$watch.options(baml.WatchOptions{ when: BadFunction2 }); +// | +// error: Error validating: Function 'BadFunction2' has incorrect type. Expected (T) -> bool, where T matches the variable type +// --> expr/watch_when.baml:5 +// | +// 4 | x.$watch.options(baml.WatchOptions{ when: BadFunction1 }); +// 5 | x.$watch.options(baml.WatchOptions{ when: BadFunction2 }); +// 6 | 1 +// | diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit.baml b/integ-tests/baml_src/test-files/workflows/workflow_emit.baml index 4f694301c1..f71c50dfc0 100644 --- a/integ-tests/baml_src/test-files/workflows/workflow_emit.baml +++ b/integ-tests/baml_src/test-files/workflows/workflow_emit.baml @@ -27,13 +27,13 @@ function AnotherTakedown(xs: string[]) -> int { // Filter function that uses an LLM to check if a word equals "banana" // This tests that filter functions can call LLMs -function IsTargetWord(prev: string, next: string) -> bool { - let result = CheckWordEquality(next, "banana"); +function IsTargetWord(word: string) -> bool { + let result = CheckWordEquality(word, "banana"); result } -function IsTargetWord2(prev: string, next: string) -> bool { - next == "banana" +function IsTargetWord2(word: string) -> bool { + word == "banana" } // LLM function that checks if two words are equal diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml index 3e5a479368..9e7ba2b869 100644 --- a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml +++ b/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml @@ -1,6 +1,6 @@ // Simple filter function that only notifies when value is not empty -function NotEmpty(prev: string, next: string) -> bool { - next != "" +function NotEmpty(value: string) -> bool { + value != "" } // Simple test without loops diff --git a/integ-tests/go/baml_client/baml_source_map.go b/integ-tests/go/baml_client/baml_source_map.go index c0875169e0..53e389156b 100644 --- a/integ-tests/go/baml_client/baml_source_map.go +++ b/integ-tests/go/baml_client/baml_source_map.go @@ -133,8 +133,8 @@ var file_map = map[string]string{ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } func getBamlFiles() map[string]string { diff --git a/integ-tests/python-v1/baml_client/async_client.py b/integ-tests/python-v1/baml_client/async_client.py index a470bf1b8b..b556516277 100644 --- a/integ-tests/python-v1/baml_client/async_client.py +++ b/integ-tests/python-v1/baml_client/async_client.py @@ -3709,34 +3709,34 @@ async def HomeEnvVarIsEmpty(self, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.IsTargetWord(prev=prev,next=next, + stream = self.stream.IsTargetWord(word=word, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.IsTargetWord2(prev=prev,next=next, + stream = self.stream.IsTargetWord2(word=word, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def IterativeFibonacci(self, n: int, @@ -3769,19 +3769,19 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.NotEmpty(prev=prev,next=next, + stream = self.stream.NotEmpty(prev=prev,value=value, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def ReturnCategory(self, category: types.Category, @@ -6907,11 +6907,11 @@ def HomeEnvVarIsEmpty(self, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -6919,11 +6919,11 @@ def IsTargetWord(self, prev: str,next: str, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -6955,11 +6955,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -8837,18 +8837,18 @@ async def HomeEnvVarIsEmpty(self, }, mode="request") return result - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result async def IterativeFibonacci(self, n: int, @@ -8865,11 +8865,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="request") return result async def ReturnCategory(self, category: types.Category, @@ -10672,18 +10672,18 @@ async def HomeEnvVarIsEmpty(self, }, mode="stream") return result - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result async def IterativeFibonacci(self, n: int, @@ -10700,11 +10700,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="stream") return result async def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python-v1/baml_client/inlinedbaml.py b/integ-tests/python-v1/baml_client/inlinedbaml.py index ea144d1c50..03001ca198 100644 --- a/integ-tests/python-v1/baml_client/inlinedbaml.py +++ b/integ-tests/python-v1/baml_client/inlinedbaml.py @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python-v1/baml_client/sync_client.py b/integ-tests/python-v1/baml_client/sync_client.py index a99548be93..9285bee939 100644 --- a/integ-tests/python-v1/baml_client/sync_client.py +++ b/integ-tests/python-v1/baml_client/sync_client.py @@ -3479,32 +3479,32 @@ def HomeEnvVarIsEmpty(self, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.IsTargetWord(prev=prev,next=next, + stream = self.stream.IsTargetWord(word=word, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.IsTargetWord2(prev=prev,next=next, + stream = self.stream.IsTargetWord2(word=word, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def IterativeFibonacci(self, n: int, @@ -3535,18 +3535,18 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.NotEmpty(prev=prev,next=next, + stream = self.stream.NotEmpty(prev=prev,value=value, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def ReturnCategory(self, category: types.Category, @@ -6658,11 +6658,11 @@ def HomeEnvVarIsEmpty(self, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -6670,11 +6670,11 @@ def IsTargetWord(self, prev: str,next: str, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -6706,11 +6706,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -8588,18 +8588,18 @@ def HomeEnvVarIsEmpty(self, }, mode="request") return result - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result def IterativeFibonacci(self, n: int, @@ -8616,11 +8616,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="request") return result def ReturnCategory(self, category: types.Category, @@ -10423,18 +10423,18 @@ def HomeEnvVarIsEmpty(self, }, mode="stream") return result - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result def IterativeFibonacci(self, n: int, @@ -10451,11 +10451,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="stream") return result def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python-v1/baml_client/watchers.py b/integ-tests/python-v1/baml_client/watchers.py index 49e7296963..8c0808467e 100644 --- a/integ-tests/python-v1/baml_client/watchers.py +++ b/integ-tests/python-v1/baml_client/watchers.py @@ -1310,6 +1310,9 @@ def __init__(self): self._lock = threading.Lock() + self._var_handlers_new_name: list[VarEventHandler[str]] = [] + self._stream_handlers_new_name: list[StreamHandler] = [] + self._var_handlers_word: list[VarEventHandler[str]] = [] self._stream_handlers_word: list[StreamHandler] = [] @@ -1317,12 +1320,16 @@ def __init__(self): self._var_handler_map: dict[str, list[VarEventHandler[Any]]] = { + "new_name": self._var_handlers_new_name, + "word": self._var_handlers_word, } self._stream_handler_map: dict[str, list[StreamHandler]] = { + "new_name": self._stream_handlers_new_name, + "word": self._stream_handlers_word, } @@ -1340,21 +1347,27 @@ def on_block(self, handler: BlockHandler) -> None: + @overload + def on_var(self, channel: Literal["new_name"], handler: VarEventHandler[str]) -> None: ... + @overload def on_var(self, channel: Literal["word"], handler: VarEventHandler[str]) -> None: ... - def on_var(self, channel: Literal["word"], handler: VarEventHandler[Any]) -> None: + def on_var(self, channel: Literal["new_name", "word"], handler: VarEventHandler[Any]) -> None: with self._lock: if channel in self._var_handler_map: self._var_handler_map[channel].append(handler) + @overload + def on_stream(self, channel: Literal["new_name"], handler: StreamHandler) -> None: ... + @overload def on_stream(self, channel: Literal["word"], handler: StreamHandler) -> None: ... - def on_stream(self, channel: Literal["word"], handler: StreamHandler) -> None: + def on_stream(self, channel: Literal["new_name", "word"], handler: StreamHandler) -> None: with self._lock: if channel in self._stream_handler_map: self._stream_handler_map[channel].append(handler) diff --git a/integ-tests/python/baml_client/async_client.py b/integ-tests/python/baml_client/async_client.py index a470bf1b8b..b556516277 100644 --- a/integ-tests/python/baml_client/async_client.py +++ b/integ-tests/python/baml_client/async_client.py @@ -3709,34 +3709,34 @@ async def HomeEnvVarIsEmpty(self, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.IsTargetWord(prev=prev,next=next, + stream = self.stream.IsTargetWord(word=word, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.IsTargetWord2(prev=prev,next=next, + stream = self.stream.IsTargetWord2(word=word, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def IterativeFibonacci(self, n: int, @@ -3769,19 +3769,19 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.NotEmpty(prev=prev,next=next, + stream = self.stream.NotEmpty(prev=prev,value=value, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def ReturnCategory(self, category: types.Category, @@ -6907,11 +6907,11 @@ def HomeEnvVarIsEmpty(self, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -6919,11 +6919,11 @@ def IsTargetWord(self, prev: str,next: str, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -6955,11 +6955,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -8837,18 +8837,18 @@ async def HomeEnvVarIsEmpty(self, }, mode="request") return result - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result async def IterativeFibonacci(self, n: int, @@ -8865,11 +8865,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="request") return result async def ReturnCategory(self, category: types.Category, @@ -10672,18 +10672,18 @@ async def HomeEnvVarIsEmpty(self, }, mode="stream") return result - async def IsTargetWord(self, prev: str,next: str, + async def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result - async def IsTargetWord2(self, prev: str,next: str, + async def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result async def IterativeFibonacci(self, n: int, @@ -10700,11 +10700,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - async def NotEmpty(self, prev: str,next: str, + async def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="stream") return result async def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python/baml_client/inlinedbaml.py b/integ-tests/python/baml_client/inlinedbaml.py index ea144d1c50..03001ca198 100644 --- a/integ-tests/python/baml_client/inlinedbaml.py +++ b/integ-tests/python/baml_client/inlinedbaml.py @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python/baml_client/sync_client.py b/integ-tests/python/baml_client/sync_client.py index a99548be93..9285bee939 100644 --- a/integ-tests/python/baml_client/sync_client.py +++ b/integ-tests/python/baml_client/sync_client.py @@ -3479,32 +3479,32 @@ def HomeEnvVarIsEmpty(self, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.IsTargetWord(prev=prev,next=next, + stream = self.stream.IsTargetWord(word=word, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.IsTargetWord2(prev=prev,next=next, + stream = self.stream.IsTargetWord2(word=word, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def IterativeFibonacci(self, n: int, @@ -3535,18 +3535,18 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.NotEmpty(prev=prev,next=next, + stream = self.stream.NotEmpty(prev=prev,value=value, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def ReturnCategory(self, category: types.Category, @@ -6658,11 +6658,11 @@ def HomeEnvVarIsEmpty(self, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -6670,11 +6670,11 @@ def IsTargetWord(self, prev: str,next: str, lambda x: typing.cast(bool, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -6706,11 +6706,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -8588,18 +8588,18 @@ def HomeEnvVarIsEmpty(self, }, mode="request") return result - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="request") return result def IterativeFibonacci(self, n: int, @@ -8616,11 +8616,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="request") return result def ReturnCategory(self, category: types.Category, @@ -10423,18 +10423,18 @@ def HomeEnvVarIsEmpty(self, }, mode="stream") return result - def IsTargetWord(self, prev: str,next: str, + def IsTargetWord(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result - def IsTargetWord2(self, prev: str,next: str, + def IsTargetWord2(self, word: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="IsTargetWord2", args={ - "prev": prev,"next": next, + "word": word, }, mode="stream") return result def IterativeFibonacci(self, n: int, @@ -10451,11 +10451,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - def NotEmpty(self, prev: str,next: str, + def NotEmpty(self, prev: str,value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"next": next, + "prev": prev,"value": value, }, mode="stream") return result def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python/baml_client/watchers.py b/integ-tests/python/baml_client/watchers.py index 49e7296963..8c0808467e 100644 --- a/integ-tests/python/baml_client/watchers.py +++ b/integ-tests/python/baml_client/watchers.py @@ -1310,6 +1310,9 @@ def __init__(self): self._lock = threading.Lock() + self._var_handlers_new_name: list[VarEventHandler[str]] = [] + self._stream_handlers_new_name: list[StreamHandler] = [] + self._var_handlers_word: list[VarEventHandler[str]] = [] self._stream_handlers_word: list[StreamHandler] = [] @@ -1317,12 +1320,16 @@ def __init__(self): self._var_handler_map: dict[str, list[VarEventHandler[Any]]] = { + "new_name": self._var_handlers_new_name, + "word": self._var_handlers_word, } self._stream_handler_map: dict[str, list[StreamHandler]] = { + "new_name": self._stream_handlers_new_name, + "word": self._stream_handlers_word, } @@ -1340,21 +1347,27 @@ def on_block(self, handler: BlockHandler) -> None: + @overload + def on_var(self, channel: Literal["new_name"], handler: VarEventHandler[str]) -> None: ... + @overload def on_var(self, channel: Literal["word"], handler: VarEventHandler[str]) -> None: ... - def on_var(self, channel: Literal["word"], handler: VarEventHandler[Any]) -> None: + def on_var(self, channel: Literal["new_name", "word"], handler: VarEventHandler[Any]) -> None: with self._lock: if channel in self._var_handler_map: self._var_handler_map[channel].append(handler) + @overload + def on_stream(self, channel: Literal["new_name"], handler: StreamHandler) -> None: ... + @overload def on_stream(self, channel: Literal["word"], handler: StreamHandler) -> None: ... - def on_stream(self, channel: Literal["word"], handler: StreamHandler) -> None: + def on_stream(self, channel: Literal["new_name", "word"], handler: StreamHandler) -> None: with self._lock: if channel in self._stream_handler_map: self._stream_handler_map[channel].append(handler) diff --git a/integ-tests/react/baml_client/async_client.ts b/integ-tests/react/baml_client/async_client.ts index 9ae88a3ee3..18209c9409 100644 --- a/integ-tests/react/baml_client/async_client.ts +++ b/integ-tests/react/baml_client/async_client.ts @@ -11713,7 +11713,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11727,7 +11727,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord( - prev,next, + word, __baml_options__ ); @@ -11743,7 +11743,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11761,7 +11761,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11775,7 +11775,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord2( - prev,next, + word, __baml_options__ ); @@ -11791,7 +11791,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,next, + prev,value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28611,7 +28611,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28652,7 +28652,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28677,7 +28677,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28718,7 +28718,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/react/baml_client/async_request.ts b/integ-tests/react/baml_client/async_request.ts index 4d27d5e4e3..52c96b775c 100644 --- a/integ-tests/react/baml_client/async_request.ts +++ b/integ-tests/react/baml_client/async_request.ts @@ -6092,7 +6092,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6103,7 +6103,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6117,7 +6117,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6128,7 +6128,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12623,7 +12623,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12634,7 +12634,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12648,7 +12648,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12659,7 +12659,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/react/baml_client/inlinedbaml.ts b/integ-tests/react/baml_client/inlinedbaml.ts index fbd3daaa4d..165eb88613 100644 --- a/integ-tests/react/baml_client/inlinedbaml.ts +++ b/integ-tests/react/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/react/baml_client/react/hooks.tsx b/integ-tests/react/baml_client/react/hooks.tsx index 9b84ac74d8..250c90128c 100644 --- a/integ-tests/react/baml_client/react/hooks.tsx +++ b/integ-tests/react/baml_client/react/hooks.tsx @@ -12464,9 +12464,7 @@ export function useHomeEnvVarIsEmpty( * * **Input Types:** * - * - prev: string - * - * - next: string + * - word: string * * * **Return Type:** @@ -12516,9 +12514,7 @@ export function useIsTargetWord( * * **Input Types:** * - * - prev: string - * - * - next: string + * - word: string * * * **Return Type:** @@ -12672,7 +12668,7 @@ export function useNormalElseIfStmt( * * - prev: string * - * - next: string + * - value: string * * * **Return Type:** diff --git a/integ-tests/react/baml_client/react/server.ts b/integ-tests/react/baml_client/react/server.ts index ad69825f70..39ba848763 100644 --- a/integ-tests/react/baml_client/react/server.ts +++ b/integ-tests/react/baml_client/react/server.ts @@ -4412,18 +4412,15 @@ export const HomeEnvVarIsEmpty = async ( * This server action calls the underlying BAML function "IsTargetWord" * with the specified parameters. * - * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } word - Input parameter. * * @returns {Promise} A promise that resolves with the result of the action. */ export const IsTargetWord = async ( - prev: string, - next: string, + word: string, ): Promise => { return b.IsTargetWord( - prev, - next, + word, ); }; @@ -4433,18 +4430,15 @@ export const IsTargetWord = async ( * This server action calls the underlying BAML function "IsTargetWord2" * with the specified parameters. * - * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } word - Input parameter. * * @returns {Promise} A promise that resolves with the result of the action. */ export const IsTargetWord2 = async ( - prev: string, - next: string, + word: string, ): Promise => { return b.IsTargetWord2( - prev, - next, + word, ); }; @@ -4494,17 +4488,17 @@ export const NormalElseIfStmt = async ( * with the specified parameters. * * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } value - Input parameter. * * @returns {Promise} A promise that resolves with the result of the action. */ export const NotEmpty = async ( prev: string, - next: string, + value: string, ): Promise => { return b.NotEmpty( prev, - next, + value, ); }; diff --git a/integ-tests/react/baml_client/react/server_streaming.ts b/integ-tests/react/baml_client/react/server_streaming.ts index a8551cea48..ea4a31b01b 100644 --- a/integ-tests/react/baml_client/react/server_streaming.ts +++ b/integ-tests/react/baml_client/react/server_streaming.ts @@ -4654,18 +4654,15 @@ export const HomeEnvVarIsEmpty = async ( * This action initiates a streaming response by calling the corresponding * BAML stream function. The returned stream yields incremental updates. * - * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } word - Input parameter. * * @returns {ReadableStream} A stream that yields incremental updates from the action. */ export const IsTargetWord = async ( - prev: string, - next: string, + word: string, ): Promise> => { const stream = b.stream.IsTargetWord( - prev, - next, + word, ); return Promise.resolve(stream.toStreamable()); }; @@ -4676,18 +4673,15 @@ export const IsTargetWord = async ( * This action initiates a streaming response by calling the corresponding * BAML stream function. The returned stream yields incremental updates. * - * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } word - Input parameter. * * @returns {ReadableStream} A stream that yields incremental updates from the action. */ export const IsTargetWord2 = async ( - prev: string, - next: string, + word: string, ): Promise> => { const stream = b.stream.IsTargetWord2( - prev, - next, + word, ); return Promise.resolve(stream.toStreamable()); }; @@ -4740,17 +4734,17 @@ export const NormalElseIfStmt = async ( * BAML stream function. The returned stream yields incremental updates. * * @param { string } prev - Input parameter. - * @param { string } next - Input parameter. + * @param { string } value - Input parameter. * * @returns {ReadableStream} A stream that yields incremental updates from the action. */ export const NotEmpty = async ( prev: string, - next: string, + value: string, ): Promise> => { const stream = b.stream.NotEmpty( prev, - next, + value, ); return Promise.resolve(stream.toStreamable()); }; diff --git a/integ-tests/react/baml_client/sync_client.ts b/integ-tests/react/baml_client/sync_client.ts index e10ce20379..19697771f0 100644 --- a/integ-tests/react/baml_client/sync_client.ts +++ b/integ-tests/react/baml_client/sync_client.ts @@ -10261,7 +10261,7 @@ export class BamlSyncClient { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10285,7 +10285,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10303,7 +10303,7 @@ export class BamlSyncClient { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10327,7 +10327,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/react/baml_client/sync_request.ts b/integ-tests/react/baml_client/sync_request.ts index faa67f8dda..940b4994ec 100644 --- a/integ-tests/react/baml_client/sync_request.ts +++ b/integ-tests/react/baml_client/sync_request.ts @@ -6088,7 +6088,7 @@ export class HttpRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6099,7 +6099,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6113,7 +6113,7 @@ export class HttpRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6124,7 +6124,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12619,7 +12619,7 @@ export class HttpStreamRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12630,7 +12630,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12644,7 +12644,7 @@ export class HttpStreamRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12655,7 +12655,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/react/baml_client/watchers.ts b/integ-tests/react/baml_client/watchers.ts index b57f797329..f743046168 100644 --- a/integ-tests/react/baml_client/watchers.ts +++ b/integ-tests/react/baml_client/watchers.ts @@ -2335,12 +2335,16 @@ export function ReturnOne(): ReturnOneEventCollector { type SimpleWatchWithFilterEventCollectorVarTypes = { + "new_name": string, + "word": string } type SimpleWatchWithFilterEventCollectorStreamTypes = { + "new_name": string | null, + "word": string | null } @@ -2351,6 +2355,8 @@ export interface SimpleWatchWithFilterEventCollector extends EventCollectorInter on_var(channel: K, handler: (event: VarNotification) => void): void + on_stream(channel: "new_name", handler: StreamHandler): void + on_stream(channel: "word", handler: StreamHandler): void @@ -2361,6 +2367,9 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const blockHandlers = new Set() + const varHandlers_new_name = new Set>() + const streamHandlers_new_name = new Set>() + const varHandlers_word = new Set>() const streamHandlers_word = new Set>() @@ -2407,12 +2416,16 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const varHandlerMap = { + "new_name": varHandlers_new_name, + "word": varHandlers_word } const streamHandlerMap = { + "new_name": streamHandlers_new_name, + "word": streamHandlers_word } diff --git a/integ-tests/typescript-esm/baml_client/async_client.ts b/integ-tests/typescript-esm/baml_client/async_client.ts index 92c1ab991a..2aac3f87ab 100644 --- a/integ-tests/typescript-esm/baml_client/async_client.ts +++ b/integ-tests/typescript-esm/baml_client/async_client.ts @@ -11713,7 +11713,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11727,7 +11727,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord( - prev,next, + word, __baml_options__ ); @@ -11743,7 +11743,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11761,7 +11761,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11775,7 +11775,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord2( - prev,next, + word, __baml_options__ ); @@ -11791,7 +11791,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,next, + prev,value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28611,7 +28611,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28652,7 +28652,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28677,7 +28677,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28718,7 +28718,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/typescript-esm/baml_client/async_request.ts b/integ-tests/typescript-esm/baml_client/async_request.ts index 977eadf0cf..c4f8f7860e 100644 --- a/integ-tests/typescript-esm/baml_client/async_request.ts +++ b/integ-tests/typescript-esm/baml_client/async_request.ts @@ -6092,7 +6092,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6103,7 +6103,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6117,7 +6117,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6128,7 +6128,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12623,7 +12623,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12634,7 +12634,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12648,7 +12648,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12659,7 +12659,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts index fbd3daaa4d..165eb88613 100644 --- a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript-esm/baml_client/sync_client.ts b/integ-tests/typescript-esm/baml_client/sync_client.ts index 9cccf308d5..ba9bfd0e68 100644 --- a/integ-tests/typescript-esm/baml_client/sync_client.ts +++ b/integ-tests/typescript-esm/baml_client/sync_client.ts @@ -10261,7 +10261,7 @@ export class BamlSyncClient { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10285,7 +10285,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10303,7 +10303,7 @@ export class BamlSyncClient { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10327,7 +10327,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/sync_request.ts b/integ-tests/typescript-esm/baml_client/sync_request.ts index a5f20c4860..8beba3f691 100644 --- a/integ-tests/typescript-esm/baml_client/sync_request.ts +++ b/integ-tests/typescript-esm/baml_client/sync_request.ts @@ -6088,7 +6088,7 @@ export class HttpRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6099,7 +6099,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6113,7 +6113,7 @@ export class HttpRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6124,7 +6124,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12619,7 +12619,7 @@ export class HttpStreamRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12630,7 +12630,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12644,7 +12644,7 @@ export class HttpStreamRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12655,7 +12655,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/watchers.ts b/integ-tests/typescript-esm/baml_client/watchers.ts index b57f797329..f743046168 100644 --- a/integ-tests/typescript-esm/baml_client/watchers.ts +++ b/integ-tests/typescript-esm/baml_client/watchers.ts @@ -2335,12 +2335,16 @@ export function ReturnOne(): ReturnOneEventCollector { type SimpleWatchWithFilterEventCollectorVarTypes = { + "new_name": string, + "word": string } type SimpleWatchWithFilterEventCollectorStreamTypes = { + "new_name": string | null, + "word": string | null } @@ -2351,6 +2355,8 @@ export interface SimpleWatchWithFilterEventCollector extends EventCollectorInter on_var(channel: K, handler: (event: VarNotification) => void): void + on_stream(channel: "new_name", handler: StreamHandler): void + on_stream(channel: "word", handler: StreamHandler): void @@ -2361,6 +2367,9 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const blockHandlers = new Set() + const varHandlers_new_name = new Set>() + const streamHandlers_new_name = new Set>() + const varHandlers_word = new Set>() const streamHandlers_word = new Set>() @@ -2407,12 +2416,16 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const varHandlerMap = { + "new_name": varHandlers_new_name, + "word": varHandlers_word } const streamHandlerMap = { + "new_name": streamHandlers_new_name, + "word": streamHandlers_word } diff --git a/integ-tests/typescript/baml_client/async_client.ts b/integ-tests/typescript/baml_client/async_client.ts index 9ae88a3ee3..18209c9409 100644 --- a/integ-tests/typescript/baml_client/async_client.ts +++ b/integ-tests/typescript/baml_client/async_client.ts @@ -11713,7 +11713,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11727,7 +11727,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord( - prev,next, + word, __baml_options__ ); @@ -11743,7 +11743,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11761,7 +11761,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11775,7 +11775,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.IsTargetWord2( - prev,next, + word, __baml_options__ ); @@ -11791,7 +11791,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,next, + prev,value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28611,7 +28611,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28652,7 +28652,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28677,7 +28677,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28718,7 +28718,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, undefined, this.ctxManager.cloneContext(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/typescript/baml_client/async_request.ts b/integ-tests/typescript/baml_client/async_request.ts index 4d27d5e4e3..52c96b775c 100644 --- a/integ-tests/typescript/baml_client/async_request.ts +++ b/integ-tests/typescript/baml_client/async_request.ts @@ -6092,7 +6092,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6103,7 +6103,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6117,7 +6117,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6128,7 +6128,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12623,7 +12623,7 @@ env?: Record } async IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12634,7 +12634,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12648,7 +12648,7 @@ env?: Record } async IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12659,7 +12659,7 @@ env?: Record return await this.runtime.buildRequest( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/inlinedbaml.ts b/integ-tests/typescript/baml_client/inlinedbaml.ts index fbd3daaa4d..165eb88613 100644 --- a/integ-tests/typescript/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(prev: string, next: string) -> bool {\n let result = CheckWordEquality(next, \"banana\");\n result\n}\n\nfunction IsTargetWord2(prev: string, next: string) -> bool {\n next == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, next: string) -> bool {\n next != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript/baml_client/sync_client.ts b/integ-tests/typescript/baml_client/sync_client.ts index e10ce20379..19697771f0 100644 --- a/integ-tests/typescript/baml_client/sync_client.ts +++ b/integ-tests/typescript/baml_client/sync_client.ts @@ -10261,7 +10261,7 @@ export class BamlSyncClient { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10285,7 +10285,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10303,7 +10303,7 @@ export class BamlSyncClient { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10327,7 +10327,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/sync_request.ts b/integ-tests/typescript/baml_client/sync_request.ts index faa67f8dda..940b4994ec 100644 --- a/integ-tests/typescript/baml_client/sync_request.ts +++ b/integ-tests/typescript/baml_client/sync_request.ts @@ -6088,7 +6088,7 @@ export class HttpRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6099,7 +6099,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6113,7 +6113,7 @@ export class HttpRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6124,7 +6124,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12619,7 +12619,7 @@ export class HttpStreamRequest { } IsTargetWord( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12630,7 +12630,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12644,7 +12644,7 @@ export class HttpStreamRequest { } IsTargetWord2( - prev: string,next: string, + word: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12655,7 +12655,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "IsTargetWord2", { - "prev": prev,"next": next + "word": word }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,next: string, + prev: string,value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"next": next + "prev": prev,"value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/watchers.ts b/integ-tests/typescript/baml_client/watchers.ts index b57f797329..f743046168 100644 --- a/integ-tests/typescript/baml_client/watchers.ts +++ b/integ-tests/typescript/baml_client/watchers.ts @@ -2335,12 +2335,16 @@ export function ReturnOne(): ReturnOneEventCollector { type SimpleWatchWithFilterEventCollectorVarTypes = { + "new_name": string, + "word": string } type SimpleWatchWithFilterEventCollectorStreamTypes = { + "new_name": string | null, + "word": string | null } @@ -2351,6 +2355,8 @@ export interface SimpleWatchWithFilterEventCollector extends EventCollectorInter on_var(channel: K, handler: (event: VarNotification) => void): void + on_stream(channel: "new_name", handler: StreamHandler): void + on_stream(channel: "word", handler: StreamHandler): void @@ -2361,6 +2367,9 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const blockHandlers = new Set() + const varHandlers_new_name = new Set>() + const streamHandlers_new_name = new Set>() + const varHandlers_word = new Set>() const streamHandlers_word = new Set>() @@ -2407,12 +2416,16 @@ export function SimpleWatchWithFilter(): SimpleWatchWithFilterEventCollector { const varHandlerMap = { + "new_name": varHandlers_new_name, + "word": varHandlers_word } const streamHandlerMap = { + "new_name": streamHandlers_new_name, + "word": streamHandlers_word } From a8a43d1203bf119f0e7944aea1ead6ceba3fd7b2 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 14:44:25 -0700 Subject: [PATCH 11/16] skip_def true --- engine/baml-compiler/src/thir/interpret.rs | 2 +- engine/baml-lib/baml/tests/hir_files/watch.baml | 8 ++++---- .../baml/tests/validation_files/expr/watch_nested.baml | 8 ++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/engine/baml-compiler/src/thir/interpret.rs b/engine/baml-compiler/src/thir/interpret.rs index 0ea99d8dc5..86e9b212f8 100644 --- a/engine/baml-compiler/src/thir/interpret.rs +++ b/engine/baml-compiler/src/thir/interpret.rs @@ -248,7 +248,7 @@ async fn check_watch_changes( crate::watch::WatchWhen::True => { // For WatchWhen::True, use built-in change detection let has_changed = match last_notified.as_ref() { - None => true, // First time, always notify + None => false, // First time (declaration), don't notify Some(last) => last != ¤t_baml_value, // Compare values }; has_changed diff --git a/engine/baml-lib/baml/tests/hir_files/watch.baml b/engine/baml-lib/baml/tests/hir_files/watch.baml index 5705719930..e28330eaad 100644 --- a/engine/baml-lib/baml/tests/hir_files/watch.baml +++ b/engine/baml-lib/baml/tests/hir_files/watch.baml @@ -8,11 +8,11 @@ function Foo() -> int { } -function MyFunctionBad(prev: int, next: int) -> bool { +function MyFunctionBad(value: int) -> bool { true } -function MyFunctionGood(prev: int, next: int) -> bool { +function MyFunctionGood(value: int) -> bool { true } @@ -26,10 +26,10 @@ function MyFunctionGood(prev: int, next: int) -> bool { // 10 // } // -// function MyFunctionBad(prev, next) { +// function MyFunctionBad(value) { // true // } // -// function MyFunctionGood(prev, next) { +// function MyFunctionGood(value) { // true // } diff --git a/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml b/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml index 3006fc9d00..746d146e0d 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml @@ -11,3 +11,11 @@ function WorkflowWatchChild() -> int { watch x: string = "Hello"; 100 } + +// error: Error validating: This line is invalid. It does not start with any known Baml schema keyword. +// --> expr/watch_nested.baml:1 +// | +// | +// 1 | function WorkflowWatch() -> int { +// 2 | watch x: int = 10; +// | From 3d506ec07660ca1471b320293d37f468b56241f9 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 15:00:34 -0700 Subject: [PATCH 12/16] cleanup extraneous channel --- engine/baml-compiler/src/hir/lowering.rs | 4 +- engine/baml-compiler/src/watch.rs | 265 ++++++++++++++++++++--- 2 files changed, 241 insertions(+), 28 deletions(-) diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index cc4eef5578..85928e7d80 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -361,8 +361,8 @@ impl Block { }) = stmt { // Extract name and when from the WatchOptions class constructor expression - let (name, when) = extract_watch_options_fields(options_expr); - watch_options_map.insert(variable.to_string(), (name, when)); + let (channel, when) = extract_watch_options_fields(options_expr); + watch_options_map.insert(variable.to_string(), (channel, when)); } } diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index c54d007209..008ceddce0 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -174,6 +174,29 @@ struct FunctionMetadata { } impl FunctionMetadata { + /// Check if the next statement is an immediate channel rename for the given variable. + /// Returns Some(new_channel_name) if the next statement is WatchOptions that: + /// 1. Operates on the same variable + /// 2. Sets a new channel name (different from the variable name) + /// Note: The presence of a `when` filter does NOT prevent this optimization. + /// If the user is setting a custom channel name, they don't want the default channel. + fn is_immediate_channel_rename( + next_statement: Option<&thir::Statement>, + var_name: &str, + ) -> Option { + if let Some(thir::Statement::WatchOptions { + variable, + channel: Some(new_channel), + .. + }) = next_statement + { + if variable == var_name && new_channel != var_name { + return Some(new_channel.clone()); + } + } + None + } + /// Add a new watch variable to the function metadata. /// If a variable with the same name already exists, /// use the existing channel and augment its type as @@ -209,8 +232,9 @@ impl FunctionMetadata { }; let thir::ExprFunction { body, .. } = function; - for statement in body.statements.iter() { - metadata.analyze_statement(statement, diagnostics); + for (idx, statement) in body.statements.iter().enumerate() { + let next_statement = body.statements.get(idx + 1); + metadata.analyze_statement(statement, next_statement, diagnostics); } metadata @@ -220,6 +244,7 @@ impl FunctionMetadata { pub fn analyze_statement( &mut self, statement: &thir::Statement, + next_statement: Option<&thir::Statement>, diagnostics: &mut Diagnostics, ) { match statement { @@ -229,8 +254,14 @@ impl FunctionMetadata { if let Some(spec) = watch { match &value.meta().1 { Some(var_type) => { - // Create a default channel with the variable's own name - self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + // Check if the next statement immediately renames the channel + let immediate_rename = + Self::is_immediate_channel_rename(next_statement, name); + + // Only create the default channel if it's not immediately renamed + if immediate_rename.is_none() { + self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + } // If the WatchSpec has a different configured name, create that channel too if &spec.name != name { @@ -240,6 +271,11 @@ impl FunctionMetadata { var_type.clone(), ); } + + // If there's an immediate rename, create that channel instead + if let Some(new_channel) = immediate_rename { + self.push_watch_var(new_channel, spec.clone(), var_type.clone()); + } } None => { diagnostics.push_error(DatamodelError::new_validation_error( @@ -267,8 +303,14 @@ impl FunctionMetadata { if let Some(spec) = watch { match &value.meta().1 { Some(var_type) => { - // Create a default channel with the variable's own name - self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + // Check if the next statement immediately renames the channel + let immediate_rename = + Self::is_immediate_channel_rename(next_statement, name); + + // Only create the default channel if it's not immediately renamed + if immediate_rename.is_none() { + self.push_watch_var(name.clone(), spec.clone(), var_type.clone()); + } // If the WatchSpec has a different configured name, create that channel too if &spec.name != name { @@ -278,6 +320,11 @@ impl FunctionMetadata { var_type.clone(), ); } + + // If there's an immediate rename, create that channel instead + if let Some(new_channel) = immediate_rename { + self.push_watch_var(new_channel, spec.clone(), var_type.clone()); + } } None => { diagnostics.push_error(DatamodelError::new_validation_error( @@ -300,17 +347,17 @@ impl FunctionMetadata { } => { self.analyze_expression(condition, diagnostics); for s in block.statements.iter() { - self.analyze_statement(s, diagnostics); + self.analyze_statement(s, None, diagnostics); } } thir::Statement::ForLoop { block, .. } => { for s in block.statements.iter() { - self.analyze_statement(s, diagnostics); + self.analyze_statement(s, None, diagnostics); } } thir::Statement::CForLoop { block, .. } => { for s in block.statements.iter() { - self.analyze_statement(s, diagnostics); + self.analyze_statement(s, None, diagnostics); } } thir::Statement::Break(_) => {} @@ -374,7 +421,7 @@ impl FunctionMetadata { thir::Expr::Builtin(_, _) => {} thir::Expr::Function(_, body, _) => { for statement in &body.statements { - self.analyze_statement(statement, diagnostics); + self.analyze_statement(statement, None, diagnostics); } } thir::Expr::If(condition, if_branch, else_branch, _) => { @@ -421,7 +468,7 @@ impl FunctionMetadata { } thir::Expr::Block(block, _) => { for stmt in block.statements.iter() { - self.analyze_statement(stmt, diagnostics); + self.analyze_statement(stmt, None, diagnostics); } } thir::Expr::BinaryOperation { left, right, .. } => { @@ -608,17 +655,17 @@ mod tests { r#" function A() -> int { watch let a_1: int = 1; - a_1.$watch.options(baml.WatchOptions{name: "a"}); + a_1.$watch.options(baml.WatchOptions{channel: "a"}); watch let a_2: string = "hi"; - a_2.$watch.options(baml.WatchOptions{name: "a"}); + a_2.$watch.options(baml.WatchOptions{channel: "a"}); watch let b_1: int | bool = true; - b_1.$watch.options(baml.WatchOptions{name: "b"}); + b_1.$watch.options(baml.WatchOptions{channel: "b"}); watch let b_2: int = 3; - b_2.$watch.options(baml.WatchOptions{name: "b"}); + b_2.$watch.options(baml.WatchOptions{channel: "b"}); watch let c_1: int = 1; - c_1.$watch.options(baml.WatchOptions{name: "c"}); + c_1.$watch.options(baml.WatchOptions{channel: "c"}); watch let c_2: int | bool = 3; - c_2.$watch.options(baml.WatchOptions{name: "c"}); + c_2.$watch.options(baml.WatchOptions{channel: "c"}); 1 } "#, @@ -691,9 +738,9 @@ mod tests { function A() -> int { watch let x = 1; x.$watch.notify(); - x.$watch.options(baml.WatchOptions{ name: "c"}); + x.$watch.options(baml.WatchOptions{ channel: "c"}); watch let y = 1; - y.$watch.options(baml.WatchOptions{ name: "c"}); + y.$watch.options(baml.WatchOptions{ channel: "c"}); 0 } "#, @@ -703,8 +750,9 @@ mod tests { let watch_channels = WatchChannels::analyze_program(&thir, &mut diagnostics); let a_channels = watch_channels.functions_channels.get("A").unwrap(); - // Should have 3 channels: "x", "y", and "c" - assert_eq!(a_channels.channels.len(), 3); + // Should have 2 channels: "x" and "c" + // "y" is NOT created because it's immediately renamed to "c" + assert_eq!(a_channels.channels.len(), 2); // Check that we have a channel named "x" assert_eq!( @@ -718,16 +766,14 @@ mod tests { 1 ); - // Check that we have a channel named "y" + // Check that we DO NOT have a channel named "y" (immediately renamed) assert_eq!( a_channels .channels .iter() - .filter(|channel| channel.0.name == "y" - && channel.0.namespace.is_none() - && channel.0.r#type == ChannelType::Variable) + .filter(|channel| channel.0.name == "y") .count(), - 1 + 0 ); // Check that we have a channel named "c" (shared by both x and y) @@ -791,4 +837,171 @@ mod tests { ); assert_eq!(hir.expr_functions.len(), 1); } + + #[test] + fn test_immediate_channel_rename_suppresses_default() { + let hir = Hir::from_source( + r#" + function A() -> int { + watch let x = 1; + x.$watch.options(baml.WatchOptions{ channel: "custom_name" }); + watch let y = 2; + y.$watch.options(baml.WatchOptions{ channel: "another_name" }); + watch let z = 3; + // z is not immediately renamed, so it should create a "z" channel + 0 + } + "#, + ); + let mut diagnostics = Diagnostics::new(PathBuf::from("test")); + let thir = typecheck(&hir, &mut diagnostics); + let watch_channels = WatchChannels::analyze_program(&thir, &mut diagnostics); + let a_channels = watch_channels.functions_channels.get("A").unwrap(); + + // Should have 3 channels: "custom_name", "another_name", and "z" + // NOT "x" or "y" since they are immediately renamed + assert_eq!(a_channels.channels.len(), 3); + + // Check that we DO NOT have a channel named "x" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "x") + .count(), + 0 + ); + + // Check that we DO NOT have a channel named "y" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "y") + .count(), + 0 + ); + + // Check that we DO have a channel named "z" (not immediately renamed) + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "z" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + + // Check that we have a channel named "custom_name" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "custom_name" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + + // Check that we have a channel named "another_name" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "another_name" + && channel.0.namespace.is_none() + && channel.0.r#type == ChannelType::Variable) + .count(), + 1 + ); + } + + #[test] + fn test_immediate_rename_with_intervening_statement() { + let hir = Hir::from_source( + r#" + function A() -> int { + watch let x = 1; + let y = 2; // Intervening statement + x.$watch.options(baml.WatchOptions{ channel: "custom_name" }); + 0 + } + "#, + ); + let mut diagnostics = Diagnostics::new(PathBuf::from("test")); + let thir = typecheck(&hir, &mut diagnostics); + let watch_channels = WatchChannels::analyze_program(&thir, &mut diagnostics); + let a_channels = watch_channels.functions_channels.get("A").unwrap(); + + // Should have both "x" and "custom_name" channels since there's an intervening statement + assert_eq!(a_channels.channels.len(), 2); + + // Check that we DO have a channel named "x" (because rename is not immediate) + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "x") + .count(), + 1 + ); + + // Check that we also have "custom_name" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "custom_name") + .count(), + 1 + ); + } + + #[test] + fn test_immediate_rename_with_when_filter() { + let hir = Hir::from_source( + r#" + function MyFilter(val: int) -> bool { + true + } + + function A() -> int { + watch let x = 1; + x.$watch.options(baml.WatchOptions{ channel: "custom", when: MyFilter }); + 0 + } + "#, + ); + let mut diagnostics = Diagnostics::new(PathBuf::from("test")); + let thir = typecheck(&hir, &mut diagnostics); + let watch_channels = WatchChannels::analyze_program(&thir, &mut diagnostics); + let a_channels = watch_channels.functions_channels.get("A").unwrap(); + + // Should have only "custom" - even with a when filter, if a custom channel is set immediately, + // the default channel is suppressed + assert_eq!(a_channels.channels.len(), 1); + + // Check that we DO NOT have a channel named "x" (suppressed by immediate rename) + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "x") + .count(), + 0 + ); + + // Check that we have "custom" + assert_eq!( + a_channels + .channels + .iter() + .filter(|channel| channel.0.name == "custom") + .count(), + 1 + ); + } } From c326c4247ce74a71f5ee9eeb6c34c1cc5381f157 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 16:10:04 -0700 Subject: [PATCH 13/16] simplify lower --- engine/baml-compiler/src/builtin.rs | 8 +- engine/baml-compiler/src/codegen.rs | 8 +- engine/baml-compiler/src/watch.rs | 10 +- engine/baml-lib/baml-core/src/ir/builtin.rs | 6 +- .../validation_files/expr/watch_nested.baml | 14 +- .../validation_files/headers/invalid.baml | 124 +++++++++--------- integ-tests/go/baml_client/baml_source_map.go | 2 +- .../python-v1/baml_client/async_client.py | 18 +-- .../python-v1/baml_client/inlinedbaml.py | 2 +- .../python-v1/baml_client/sync_client.py | 18 +-- .../python/baml_client/async_client.py | 18 +-- integ-tests/python/baml_client/inlinedbaml.py | 2 +- integ-tests/python/baml_client/sync_client.py | 18 +-- integ-tests/react/baml_client/async_client.ts | 10 +- .../react/baml_client/async_request.ts | 8 +- integ-tests/react/baml_client/inlinedbaml.ts | 2 +- integ-tests/react/baml_client/react/hooks.tsx | 2 - integ-tests/react/baml_client/react/server.ts | 3 - .../baml_client/react/server_streaming.ts | 3 - integ-tests/react/baml_client/sync_client.ts | 4 +- integ-tests/react/baml_client/sync_request.ts | 8 +- .../baml_client/async_client.ts | 10 +- .../baml_client/async_request.ts | 8 +- .../typescript-esm/baml_client/inlinedbaml.ts | 2 +- .../typescript-esm/baml_client/sync_client.ts | 4 +- .../baml_client/sync_request.ts | 8 +- .../typescript/baml_client/async_client.ts | 10 +- .../typescript/baml_client/async_request.ts | 8 +- .../typescript/baml_client/inlinedbaml.ts | 2 +- .../typescript/baml_client/sync_client.ts | 4 +- .../typescript/baml_client/sync_request.ts | 8 +- 31 files changed, 169 insertions(+), 183 deletions(-) diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index e6d4a1107d..5a07da74c7 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -45,18 +45,18 @@ pub fn builtin_classes() -> Vec { methods: vec![], fields: vec![ Field { - name: String::from("name"), + name: String::from("channel"), r#type: TypeIR::optional(TypeIR::string()), span: Span::fake(), }, Field { name: String::from("when"), - // "never" | "manual" | ((T, T) -> bool) - // We use a generic function type with top types for T + // "never" | "manual" | (T -> bool) + // We use a generic function type with top type for T r#type: TypeIR::optional(TypeIR::union(vec![ TypeIR::literal_string("never".to_string()), TypeIR::literal_string("manual".to_string()), - TypeIR::arrow(vec![TypeIR::top(), TypeIR::top()], TypeIR::bool()), + TypeIR::arrow(vec![TypeIR::top()], TypeIR::bool()), ])), span: Span::fake(), }, diff --git a/engine/baml-compiler/src/codegen.rs b/engine/baml-compiler/src/codegen.rs index d5cbd1a3ba..23936e6565 100644 --- a/engine/baml-compiler/src/codegen.rs +++ b/engine/baml-compiler/src/codegen.rs @@ -906,9 +906,11 @@ impl<'g> HirCompiler<'g> { self.compile_expression(condition); self.emit(Instruction::Assert); } - thir::Statement::WatchOptions { .. } | thir::Statement::WatchNotify { .. } => { - // These are handled at the interpreter level, not in bytecode - // They update runtime watch specs and don't need bytecode generation + thir::Statement::WatchOptions { .. } => { + todo!("bytecode codegen update to variable's WatchOptions") + } + thir::Statement::WatchNotify { .. } => { + todo!("bytecode codegen for manual notification trigger") } } } diff --git a/engine/baml-compiler/src/watch.rs b/engine/baml-compiler/src/watch.rs index 008ceddce0..8ba3f3f7d2 100644 --- a/engine/baml-compiler/src/watch.rs +++ b/engine/baml-compiler/src/watch.rs @@ -178,8 +178,6 @@ impl FunctionMetadata { /// Returns Some(new_channel_name) if the next statement is WatchOptions that: /// 1. Operates on the same variable /// 2. Sets a new channel name (different from the variable name) - /// Note: The presence of a `when` filter does NOT prevent this optimization. - /// If the user is setting a custom channel name, they don't want the default channel. fn is_immediate_channel_rename( next_statement: Option<&thir::Statement>, var_name: &str, @@ -809,11 +807,11 @@ mod tests { let hir = Hir::from_source( r#" class WatchOptions { - name string + channel string } function A() -> int { - let opts = baml.WatchOptions{name: "test"}; + let opts = baml.WatchOptions{channel: "test"}; 1 } "#, @@ -826,11 +824,11 @@ mod tests { let hir = Hir::from_source( r#" class WatchOptions { - name string + channel string } function A() -> int { - let opts = WatchOptions{name: "test"}; + let opts = WatchOptions{channel: "test"}; 1 } "#, diff --git a/engine/baml-lib/baml-core/src/ir/builtin.rs b/engine/baml-lib/baml-core/src/ir/builtin.rs index 46723a4ef2..4bb2e89e86 100644 --- a/engine/baml-lib/baml-core/src/ir/builtin.rs +++ b/engine/baml-lib/baml-core/src/ir/builtin.rs @@ -8,15 +8,15 @@ use super::repr::{Class, Enum, EnumValue, ExprFunction, Field, Node, NodeAttribu use crate::{ir::repr::IntermediateRepr, Configuration}; pub mod functions { - pub const FETCH_VALUE: &str = "std::fetch_value"; + pub const FETCH_VALUE: &str = "std.fetch_value"; } pub mod classes { - pub const REQUEST: &str = "std::Request"; + pub const REQUEST: &str = "std.Request"; } pub mod enums { - pub const HTTP_METHOD: &str = "std::HttpMethod"; + pub const HTTP_METHOD: &str = "std.HttpMethod"; } /// Builtins are exposed through a separate IR, which can be combined with diff --git a/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml b/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml index 746d146e0d..ebd3e95654 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/watch_nested.baml @@ -1,6 +1,6 @@ function WorkflowWatch() -> int { - watch x: int = 10; - watch y: bool = true; + watch let x: int = 10; + watch let y: bool = true; y = false; x = WorkflowWatchChild(); @@ -8,14 +8,6 @@ function WorkflowWatch() -> int { } function WorkflowWatchChild() -> int { - watch x: string = "Hello"; + watch let x: string = "Hello"; 100 } - -// error: Error validating: This line is invalid. It does not start with any known Baml schema keyword. -// --> expr/watch_nested.baml:1 -// | -// | -// 1 | function WorkflowWatch() -> int { -// 2 | watch x: int = 10; -// | diff --git a/engine/baml-lib/baml/tests/validation_files/headers/invalid.baml b/engine/baml-lib/baml/tests/validation_files/headers/invalid.baml index 91da8bad7a..e911b4b77a 100644 --- a/engine/baml-lib/baml/tests/validation_files/headers/invalid.baml +++ b/engine/baml-lib/baml/tests/validation_files/headers/invalid.baml @@ -1,61 +1,63 @@ -// This file contains invalid MDX header usage patterns - -function HeaderInPrompt() -> string { - client GPT4 - prompt #" - //# This is inside a prompt - It should be treated as literal text, not a header - "# -} - -function MixedOnSameLine() -> string { - let x = "hello" ## Inline Header // Should fail - x -} - -function HeaderWithoutSpace() -> string { - //#No Space After Hash // Should potentially fail depending on grammar rules - let x = "test" - x -} - -function EmptyHeader() -> string { - //# - // Empty header title - this should fail - let x = "test" - //## - // Another empty header - x -} - -// Test headers in places they shouldn't parse -function HeaderInExpression() -> string { - let x = "start" + # Middle Header // Should fail - "end" - x -} - -function HeaderInStringLiteral() -> string { - let x = "This string contains # Header Text" - let y = #" - Raw string with # Header - Should not be parsed - "# - x + y -} - -// LLM function (not expression function) - headers should not be allowed -function LLMFunction(input: string) -> string { - client GPT4 - //# Section Header - // Should fail - only allowed in expression functions - prompt #"Process: {{ input }}"# -} - -client GPT4 { - provider openai - options { - model gpt-4 - api_key env.OPENAI_API_KEY - } -} +// TODO: Bring these tests back. +// // This file contains invalid MDX header usage patterns +// +// function HeaderInPrompt() -> string { +// client GPT4 +// prompt #" +// //# This is inside a prompt +// It should be treated as literal text, not a header +// "# +// } +// +// function MixedOnSameLine() -> string { +// let x = "hello" ## Inline Header // Should fail +// x +// } +// +// function HeaderWithoutSpace() -> string { +// //#No Space After Hash // Should potentially fail depending on grammar rules +// let x = "test" +// x +// } +// +// function EmptyHeader() -> string { +// //# +// // Empty header title - this should fail +// let x = "test" +// //## +// // Another empty header +// x +// } +// +// // Test headers in places they shouldn't parse +// function HeaderInExpression() -> string { +// let x = "start" + # Middle Header // Should fail +// "end" +// x +// } +// +// function HeaderInStringLiteral() -> string { +// let x = "This string contains # Header Text" +// let y = #" +// Raw string with # Header +// Should not be parsed +// "# +// x + y +// } +// +// // LLM function (not expression function) - headers should not be allowed +// function LLMFunction(input: string) -> string { +// client GPT4 +// //# Section Header +// // Should fail - only allowed in expression functions +// prompt #"Process: {{ input }}"# +// } +// +// client GPT4 { +// provider openai +// options { +// model gpt-4 +// api_key env.OPENAI_API_KEY +// } +// } +// diff --git a/integ-tests/go/baml_client/baml_source_map.go b/integ-tests/go/baml_client/baml_source_map.go index 53e389156b..8688efae6a 100644 --- a/integ-tests/go/baml_client/baml_source_map.go +++ b/integ-tests/go/baml_client/baml_source_map.go @@ -134,7 +134,7 @@ var file_map = map[string]string{ "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } func getBamlFiles() map[string]string { diff --git a/integ-tests/python-v1/baml_client/async_client.py b/integ-tests/python-v1/baml_client/async_client.py index b556516277..c712da1b9f 100644 --- a/integ-tests/python-v1/baml_client/async_client.py +++ b/integ-tests/python-v1/baml_client/async_client.py @@ -3769,19 +3769,19 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.NotEmpty(prev=prev,value=value, + stream = self.stream.NotEmpty(value=value, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def ReturnCategory(self, category: types.Category, @@ -6955,11 +6955,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -8865,11 +8865,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="request") return result async def ReturnCategory(self, category: types.Category, @@ -10700,11 +10700,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="stream") return result async def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python-v1/baml_client/inlinedbaml.py b/integ-tests/python-v1/baml_client/inlinedbaml.py index 03001ca198..799bc8ce1c 100644 --- a/integ-tests/python-v1/baml_client/inlinedbaml.py +++ b/integ-tests/python-v1/baml_client/inlinedbaml.py @@ -131,7 +131,7 @@ "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python-v1/baml_client/sync_client.py b/integ-tests/python-v1/baml_client/sync_client.py index 9285bee939..48bfbb6357 100644 --- a/integ-tests/python-v1/baml_client/sync_client.py +++ b/integ-tests/python-v1/baml_client/sync_client.py @@ -3535,18 +3535,18 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.NotEmpty(prev=prev,value=value, + stream = self.stream.NotEmpty(value=value, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def ReturnCategory(self, category: types.Category, @@ -6706,11 +6706,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -8616,11 +8616,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="request") return result def ReturnCategory(self, category: types.Category, @@ -10451,11 +10451,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="stream") return result def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python/baml_client/async_client.py b/integ-tests/python/baml_client/async_client.py index b556516277..c712da1b9f 100644 --- a/integ-tests/python/baml_client/async_client.py +++ b/integ-tests/python/baml_client/async_client.py @@ -3769,19 +3769,19 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: # Use streaming internally when on_tick is provided - stream = self.stream.NotEmpty(prev=prev,value=value, + stream = self.stream.NotEmpty(value=value, baml_options=baml_options) return await stream.get_final_response() else: # Original non-streaming code result = await self.__options.merge_options(baml_options).call_function_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) async def ReturnCategory(self, category: types.Category, @@ -6955,11 +6955,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_async_stream(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return baml_py.BamlStream[typing.Optional[bool], bool]( result, @@ -8865,11 +8865,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="request") return result async def ReturnCategory(self, category: types.Category, @@ -10700,11 +10700,11 @@ async def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - async def NotEmpty(self, prev: str,value: str, + async def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = await self.__options.merge_options(baml_options).create_http_request_async(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="stream") return result async def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/python/baml_client/inlinedbaml.py b/integ-tests/python/baml_client/inlinedbaml.py index 03001ca198..799bc8ce1c 100644 --- a/integ-tests/python/baml_client/inlinedbaml.py +++ b/integ-tests/python/baml_client/inlinedbaml.py @@ -131,7 +131,7 @@ "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python/baml_client/sync_client.py b/integ-tests/python/baml_client/sync_client.py index 9285bee939..48bfbb6357 100644 --- a/integ-tests/python/baml_client/sync_client.py +++ b/integ-tests/python/baml_client/sync_client.py @@ -3535,18 +3535,18 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }) return typing.cast(int, result.cast_to(types, types, stream_types, False, __runtime__)) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> bool: # Check if on_tick is provided if 'on_tick' in baml_options: - stream = self.stream.NotEmpty(prev=prev,value=value, + stream = self.stream.NotEmpty(value=value, baml_options=baml_options) return stream.get_final_response() else: # Original non-streaming code result = self.__options.merge_options(baml_options).call_function_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return typing.cast(bool, result.cast_to(types, types, stream_types, False, __runtime__)) def ReturnCategory(self, category: types.Category, @@ -6706,11 +6706,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, lambda x: typing.cast(int, x.cast_to(types, types, stream_types, False, __runtime__)), ctx, ) - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.BamlSyncStream[typing.Optional[bool], bool]: ctx, result = self.__options.merge_options(baml_options).create_sync_stream(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }) return baml_py.BamlSyncStream[typing.Optional[bool], bool]( result, @@ -8616,11 +8616,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="request") return result - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="request") return result def ReturnCategory(self, category: types.Category, @@ -10451,11 +10451,11 @@ def NormalElseIfStmt(self, a: bool,b: bool, "a": a,"b": b, }, mode="stream") return result - def NotEmpty(self, prev: str,value: str, + def NotEmpty(self, value: str, baml_options: BamlCallOptions = {}, ) -> baml_py.baml_py.HTTPRequest: result = self.__options.merge_options(baml_options).create_http_request_sync(function_name="NotEmpty", args={ - "prev": prev,"value": value, + "value": value, }, mode="stream") return result def ReturnCategory(self, category: types.Category, diff --git a/integ-tests/react/baml_client/async_client.ts b/integ-tests/react/baml_client/async_client.ts index 18209c9409..394411d8ee 100644 --- a/integ-tests/react/baml_client/async_client.ts +++ b/integ-tests/react/baml_client/async_client.ts @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,value, + value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/react/baml_client/async_request.ts b/integ-tests/react/baml_client/async_request.ts index 52c96b775c..72d685e170 100644 --- a/integ-tests/react/baml_client/async_request.ts +++ b/integ-tests/react/baml_client/async_request.ts @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/react/baml_client/inlinedbaml.ts b/integ-tests/react/baml_client/inlinedbaml.ts index 165eb88613..7b4b79f05e 100644 --- a/integ-tests/react/baml_client/inlinedbaml.ts +++ b/integ-tests/react/baml_client/inlinedbaml.ts @@ -139,7 +139,7 @@ const fileMap = { "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/react/baml_client/react/hooks.tsx b/integ-tests/react/baml_client/react/hooks.tsx index 250c90128c..0536e3db2a 100644 --- a/integ-tests/react/baml_client/react/hooks.tsx +++ b/integ-tests/react/baml_client/react/hooks.tsx @@ -12666,8 +12666,6 @@ export function useNormalElseIfStmt( * * **Input Types:** * - * - prev: string - * * - value: string * * diff --git a/integ-tests/react/baml_client/react/server.ts b/integ-tests/react/baml_client/react/server.ts index 39ba848763..ad732ae937 100644 --- a/integ-tests/react/baml_client/react/server.ts +++ b/integ-tests/react/baml_client/react/server.ts @@ -4487,17 +4487,14 @@ export const NormalElseIfStmt = async ( * This server action calls the underlying BAML function "NotEmpty" * with the specified parameters. * - * @param { string } prev - Input parameter. * @param { string } value - Input parameter. * * @returns {Promise} A promise that resolves with the result of the action. */ export const NotEmpty = async ( - prev: string, value: string, ): Promise => { return b.NotEmpty( - prev, value, ); }; diff --git a/integ-tests/react/baml_client/react/server_streaming.ts b/integ-tests/react/baml_client/react/server_streaming.ts index ea4a31b01b..a446b04eb5 100644 --- a/integ-tests/react/baml_client/react/server_streaming.ts +++ b/integ-tests/react/baml_client/react/server_streaming.ts @@ -4733,17 +4733,14 @@ export const NormalElseIfStmt = async ( * This action initiates a streaming response by calling the corresponding * BAML stream function. The returned stream yields incremental updates. * - * @param { string } prev - Input parameter. * @param { string } value - Input parameter. * * @returns {ReadableStream} A stream that yields incremental updates from the action. */ export const NotEmpty = async ( - prev: string, value: string, ): Promise> => { const stream = b.stream.NotEmpty( - prev, value, ); return Promise.resolve(stream.toStreamable()); diff --git a/integ-tests/react/baml_client/sync_client.ts b/integ-tests/react/baml_client/sync_client.ts index 19697771f0..c005711c2c 100644 --- a/integ-tests/react/baml_client/sync_client.ts +++ b/integ-tests/react/baml_client/sync_client.ts @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/react/baml_client/sync_request.ts b/integ-tests/react/baml_client/sync_request.ts index 940b4994ec..eeb77a62b1 100644 --- a/integ-tests/react/baml_client/sync_request.ts +++ b/integ-tests/react/baml_client/sync_request.ts @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/async_client.ts b/integ-tests/typescript-esm/baml_client/async_client.ts index 2aac3f87ab..50a6824b87 100644 --- a/integ-tests/typescript-esm/baml_client/async_client.ts +++ b/integ-tests/typescript-esm/baml_client/async_client.ts @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,value, + value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/typescript-esm/baml_client/async_request.ts b/integ-tests/typescript-esm/baml_client/async_request.ts index c4f8f7860e..ed9e0d931f 100644 --- a/integ-tests/typescript-esm/baml_client/async_request.ts +++ b/integ-tests/typescript-esm/baml_client/async_request.ts @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts index 165eb88613..7b4b79f05e 100644 --- a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts @@ -139,7 +139,7 @@ const fileMap = { "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript-esm/baml_client/sync_client.ts b/integ-tests/typescript-esm/baml_client/sync_client.ts index ba9bfd0e68..572095b49c 100644 --- a/integ-tests/typescript-esm/baml_client/sync_client.ts +++ b/integ-tests/typescript-esm/baml_client/sync_client.ts @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/typescript-esm/baml_client/sync_request.ts b/integ-tests/typescript-esm/baml_client/sync_request.ts index 8beba3f691..ae42e1d422 100644 --- a/integ-tests/typescript-esm/baml_client/sync_request.ts +++ b/integ-tests/typescript-esm/baml_client/sync_request.ts @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/async_client.ts b/integ-tests/typescript/baml_client/async_client.ts index 18209c9409..394411d8ee 100644 --- a/integ-tests/typescript/baml_client/async_client.ts +++ b/integ-tests/typescript/baml_client/async_client.ts @@ -11905,7 +11905,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -11919,7 +11919,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull // Check if onTick is provided - route through streaming if so if (options.onTick) { const stream = this.stream.NotEmpty( - prev,value, + value, __baml_options__ ); @@ -11935,7 +11935,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = await this.runtime.callFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), @@ -28875,7 +28875,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): BamlStream { @@ -28916,7 +28916,7 @@ export type RecursivePartialNull = MovedRecursivePartialNull const raw = this.runtime.streamFunction( "NotEmpty", { - "prev": prev,"value": value + "value": value }, undefined, this.ctxManager.cloneContext(), diff --git a/integ-tests/typescript/baml_client/async_request.ts b/integ-tests/typescript/baml_client/async_request.ts index 52c96b775c..72d685e170 100644 --- a/integ-tests/typescript/baml_client/async_request.ts +++ b/integ-tests/typescript/baml_client/async_request.ts @@ -6192,7 +6192,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -6203,7 +6203,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12723,7 +12723,7 @@ env?: Record } async NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): Promise { try { @@ -12734,7 +12734,7 @@ env?: Record return await this.runtime.buildRequest( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/inlinedbaml.ts b/integ-tests/typescript/baml_client/inlinedbaml.ts index 165eb88613..7b4b79f05e 100644 --- a/integ-tests/typescript/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript/baml_client/inlinedbaml.ts @@ -139,7 +139,7 @@ const fileMap = { "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(prev: string, value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript/baml_client/sync_client.ts b/integ-tests/typescript/baml_client/sync_client.ts index 19697771f0..c005711c2c 100644 --- a/integ-tests/typescript/baml_client/sync_client.ts +++ b/integ-tests/typescript/baml_client/sync_client.ts @@ -10429,7 +10429,7 @@ export class BamlSyncClient { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): boolean { try { @@ -10453,7 +10453,7 @@ export class BamlSyncClient { const raw = this.runtime.callFunctionSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), options.tb?.__tb(), diff --git a/integ-tests/typescript/baml_client/sync_request.ts b/integ-tests/typescript/baml_client/sync_request.ts index 940b4994ec..eeb77a62b1 100644 --- a/integ-tests/typescript/baml_client/sync_request.ts +++ b/integ-tests/typescript/baml_client/sync_request.ts @@ -6188,7 +6188,7 @@ export class HttpRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -6199,7 +6199,7 @@ export class HttpRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), @@ -12719,7 +12719,7 @@ export class HttpStreamRequest { } NotEmpty( - prev: string,value: string, + value: string, __baml_options__?: BamlCallOptions ): HTTPRequest { try { @@ -12730,7 +12730,7 @@ export class HttpStreamRequest { return this.runtime.buildRequestSync( "NotEmpty", { - "prev": prev,"value": value + "value": value }, this.ctxManager.cloneContext(), __baml_options__?.tb?.__tb(), From e15c40645f1752de62afd31f07b56e371893fc65 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 18:58:03 -0700 Subject: [PATCH 14/16] namespace is . not :: --- engine/baml-compiler/src/builtin.rs | 8 +- engine/baml-compiler/src/thir/typecheck.rs | 136 +++++++++++++++--- engine/baml-lib/ast/src/parser/parse_expr.rs | 5 +- .../tests/validation_files/expr/builtin.baml | 26 +--- .../expr/constructors_invalid.baml | 20 +-- .../strings/unquoted_strings.baml | 64 +-------- engine/baml-runtime/tests/test_runtime.rs | 2 +- 7 files changed, 143 insertions(+), 118 deletions(-) diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index 5a07da74c7..0567a16e58 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -5,6 +5,7 @@ use crate::hir::{Class, Enum, EnumVariant, Field}; pub mod functions { pub const FETCH_AS: &str = "baml.fetch_as"; + pub const FETCH_VALUE: &str = "std.fetch_value"; } pub mod classes { @@ -82,11 +83,16 @@ pub fn std_request_type() -> TypeIR { TypeIR::class(classes::REQUEST) } -/// Create a function signature for std::fetch_value +/// Create a function signature for baml.fetch_as pub fn baml_fetch_as_signature(return_type: TypeIR) -> TypeIR { TypeIR::arrow(vec![TypeIR::string()], return_type) } +/// Create a function signature for std.fetch_value +pub fn std_fetch_value_signature(return_type: TypeIR) -> TypeIR { + TypeIR::arrow(vec![TypeIR::class(classes::REQUEST)], return_type) +} + pub fn is_builtin_identifier(identifier: &str) -> bool { identifier.starts_with("std::") || identifier.starts_with("baml::") diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 20aef6b8b4..9ce3c6fd75 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -92,15 +92,20 @@ pub fn typecheck_returning_context<'a>( } // Add builtin functions to typing context - // std::fetch_value(std::Request) -> T - // This is a generic function that takes a Request and returns any type T - // For now, we'll add a placeholder with a Top type. + // baml.fetch_as(url: string) -> T + // std.fetch_value(request: std.Request) -> T + // These are generic functions. For now, we'll add a placeholder with a Top type. let generic_return_type = TypeIR::Top(Default::default()); // Placeholder for generic T - let fetch_as_type = crate::builtin::baml_fetch_as_signature(generic_return_type); + let fetch_as_type = crate::builtin::baml_fetch_as_signature(generic_return_type.clone()); typing_context.symbols.insert( crate::builtin::functions::FETCH_AS.to_string(), fetch_as_type, ); + let fetch_value_type = crate::builtin::std_fetch_value_signature(generic_return_type); + typing_context.symbols.insert( + crate::builtin::functions::FETCH_VALUE.to_string(), + fetch_value_type, + ); // Add native functions to typing context let native_fns = baml_vm::native::functions(); @@ -1503,9 +1508,17 @@ pub fn typecheck_expression( let func_type = context.get_type(&func_name).cloned(); // TODO: Handle generics uniformly, not with this kind of one-off handler. - if func_name == crate::builtin::functions::FETCH_AS && type_args.is_empty() { + if (func_name == crate::builtin::functions::FETCH_AS + || func_name == crate::builtin::functions::FETCH_VALUE) + && type_args.is_empty() + { + let fn_name_display = if func_name == crate::builtin::functions::FETCH_AS { + "baml.fetch_as" + } else { + "std.fetch_value" + }; diagnostics.push_error(DatamodelError::new_validation_error( - "Generic function std.fetch_value must have a type argument. Try adding a type argument like this: std.fetch_value", + &format!("Generic function {} must have a type argument. Try adding a type argument like this: {}", fn_name_display, fn_name_display), function.span().clone(), )); } @@ -1607,6 +1620,7 @@ pub fn typecheck_expression( ("env", "get") => Some("env.get"), ("baml", "deep_copy") => Some("baml.deep_copy"), ("baml", "fetch_as") => Some("baml.fetch_as"), + ("std", "fetch_value") => Some("std.fetch_value"), ("image", "from_url") => Some("baml.media.image.from_url"), ("audio", "from_url") => Some("baml.media.audio.from_url"), ("video", "from_url") => Some("baml.media.video.from_url"), @@ -1628,6 +1642,22 @@ pub fn typecheck_expression( let mut return_type = None; + // Validate type arguments for generic functions + if (full_name == crate::builtin::functions::FETCH_AS + || full_name == crate::builtin::functions::FETCH_VALUE) + && type_args.is_empty() + { + let fn_name_display = if full_name == crate::builtin::functions::FETCH_AS { + "baml.fetch_as" + } else { + "std.fetch_value" + }; + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Generic function {} must have a type argument. Try adding a type argument like this: {}", fn_name_display, fn_name_display), + span.clone(), + )); + } + // Handle generic functions with special type inference match full_name { "baml.deep_copy" => { @@ -1836,6 +1866,7 @@ pub fn typecheck_expression( ("baml", "deep_copy") => Some("baml.deep_copy".to_string()), ("baml", "fetch_as") => Some("baml.fetch_as".to_string()), + ("std", "fetch_value") => Some("std.fetch_value".to_string()), ("baml.unstable", "string") => Some("baml.unstable.string".to_string()), @@ -1886,6 +1917,22 @@ pub fn typecheck_expression( } }; + // Validate type arguments for generic functions + if (full_name == crate::builtin::functions::FETCH_AS + || full_name == crate::builtin::functions::FETCH_VALUE) + && type_args.is_empty() + { + let fn_name_display = if full_name == crate::builtin::functions::FETCH_AS { + "baml.fetch_as" + } else { + "std.fetch_value" + }; + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Generic function {} must have a type argument. Try adding a type argument like this: {}", fn_name_display, fn_name_display), + span.clone(), + )); + } + // image.from_url is not a "method", it's an associated function (kind of). let is_function_call_on_namespace = matches!( &typed_receiver, @@ -1940,19 +1987,25 @@ pub fn typecheck_expression( )); } "baml.fetch_as" => { - generic_return_type_inferred = match &type_args[0] { - hir::TypeArg::Type(t) => Some(t.to_owned()), - hir::TypeArg::TypeName(n) => context - .classes - .get(n) - .map(|c| TypeIR::class(c.name.clone())) - .or_else(|| { - context - .enums - .get(n) - .map(|e| TypeIR::r#enum(&e.name)) - }) - .or_else(|| context.get_type(n).map(|t| t.to_owned())), + generic_return_type_inferred = if !type_args.is_empty() { + match &type_args[0] { + hir::TypeArg::Type(t) => Some(t.to_owned()), + hir::TypeArg::TypeName(n) => context + .classes + .get(n) + .map(|c| TypeIR::class(c.name.clone())) + .or_else(|| { + context + .enums + .get(n) + .map(|e| TypeIR::r#enum(&e.name)) + }) + .or_else(|| { + context.get_type(n).map(|t| t.to_owned()) + }), + } + } else { + None }; match &generic_return_type_inferred { @@ -1973,6 +2026,47 @@ pub fn typecheck_expression( } } } + "std.fetch_value" => { + generic_return_type_inferred = if !type_args.is_empty() { + match &type_args[0] { + hir::TypeArg::Type(t) => Some(t.to_owned()), + hir::TypeArg::TypeName(n) => context + .classes + .get(n) + .map(|c| TypeIR::class(c.name.clone())) + .or_else(|| { + context + .enums + .get(n) + .map(|e| TypeIR::r#enum(&e.name)) + }) + .or_else(|| { + context.get_type(n).map(|t| t.to_owned()) + }), + } + } else { + None + }; + + match &generic_return_type_inferred { + Some(t) => { + func_type = Some(TypeIR::arrow( + vec![TypeIR::class( + crate::builtin::classes::REQUEST, + )], + t.clone(), + )); + } + + None => { + diagnostics + .push_error(DatamodelError::new_validation_error( + "could not infer return type of std.fetch_value", + arg.span(), + )); + } + } + } _ => { if !types_compatible(arg_type, expected_type) { diagnostics.push_error( @@ -2010,7 +2104,7 @@ pub fn typecheck_expression( &typed_receiver, thir::Expr::Var(name, _) if matches!( name.as_str(), - "image" | "audio" | "video" | "pdf" | "baml" | "baml.unstable" + "image" | "audio" | "video" | "pdf" | "baml" | "baml.unstable" | "std" ) ); @@ -2042,7 +2136,7 @@ pub fn typecheck_expression( full_name.clone(), (span.clone(), func_type.clone()), )), - type_args: if full_name == "baml.fetch_as" + type_args: if (full_name == "baml.fetch_as" || full_name == "std.fetch_value") && generic_return_type_inferred.is_some() { vec![generic_return_type_inferred.clone().unwrap()] diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs b/engine/baml-lib/ast/src/parser/parse_expr.rs index 65e362e838..7a0eb68e7b 100644 --- a/engine/baml-lib/ast/src/parser/parse_expr.rs +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs @@ -27,7 +27,7 @@ pub fn parse_expr_fn(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option int", span.clone(), )); + if matches!(arrow_or_body.as_rule(), Rule::SPACER_TEXT) { + arrow_or_body = tokens.next()?; + } let function_body = parse_function_body(arrow_or_body, diagnostics); (None, function_body) }; diff --git a/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml b/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml index 84b5e719c5..42298a2a34 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/builtin.baml @@ -21,33 +21,13 @@ function GetTodoMissingTypeArg() -> Todo { }) } -// error: Generic function std.fetch_value must have a type argument. Try adding a type argument like this: std.fetch_value +// error: Error validating: Generic function std.fetch_value must have a type argument. Try adding a type argument like this: std.fetch_value // --> expr/builtin.baml:17 -// | +// | // 16 | function GetTodoMissingTypeArg() -> Todo { // 17 | std.fetch_value(std.Request { // 18 | base_url: "https://dummyjson.com/todos/1", // 19 | headers: {}, // 20 | query_params: {}, // 21 | }) -// | -// error: Error validating: Unknown function std.fetch_value -// --> expr/builtin.baml:9 -// | -// 8 | function GetTodo() -> Todo { -// 9 | std.fetch_value(std.Request { -// 10 | base_url: "https://dummyjson.com/todos/1", -// 11 | headers: {}, -// 12 | query_params: {}, -// 13 | }) -// | -// error: Error validating: Unknown function std.fetch_value -// --> expr/builtin.baml:17 -// | -// 16 | function GetTodoMissingTypeArg() -> Todo { -// 17 | std.fetch_value(std.Request { -// 18 | base_url: "https://dummyjson.com/todos/1", -// 19 | headers: {}, -// 20 | query_params: {}, -// 21 | }) -// | +// | diff --git a/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml b/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml index 2f2cf77ed5..2e034f2097 100644 --- a/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml +++ b/engine/baml-lib/baml/tests/validation_files/expr/constructors_invalid.baml @@ -20,31 +20,31 @@ function Foo() -> Bar { // error: Error validating: Bar.a expected type int, but found string // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | +// | // error: Error validating: Class Bar has no field c // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | +// | // error: Error validating: Class Bar is missing fields: b // --> expr/constructors_invalid.baml:7 -// | +// | // 6 | function Foo() -> Bar { // 7 | let x = Bar { a: "hello", c: 12 }; -// | +// | // error: Error validating: std.Request.base_url expected type string, but found int // --> expr/constructors_invalid.baml:9 -// | +// | // 8 | let req_bad = std.Request { // 9 | base_url: 10, -// | +// | // error: Error validating: std.Request.query_params expected type map, but found map // --> expr/constructors_invalid.baml:11 -// | +// | // 10 | headers: {}, // 11 | query_params: { a 10 }, -// | +// | diff --git a/engine/baml-lib/baml/tests/validation_files/strings/unquoted_strings.baml b/engine/baml-lib/baml/tests/validation_files/strings/unquoted_strings.baml index 0f682d3d8c..7da1b61bdf 100644 --- a/engine/baml-lib/baml/tests/validation_files/strings/unquoted_strings.baml +++ b/engine/baml-lib/baml/tests/validation_files/strings/unquoted_strings.baml @@ -8,68 +8,10 @@ client Hello { } } -// error: Error validating Client "Hello": This field declaration is invalid. It is either missing a name or a type. -// --> strings/unquoted_strings.baml:3 -// | -// 2 | provider baml-openai-chat -// 3 | options { -// | -// error: Error validating: This line is not a valid field or attribute definition. A valid property may look like: 'myProperty "some value"' for example, with no colons. -// --> strings/unquoted_strings.baml:3 -// | -// 2 | provider baml-openai-chat -// 3 | options { -// 4 | thing hello'world -// | -// error: Error validating: This line is not a valid field or attribute definition. A valid property may look like: 'myProperty "some value"' for example, with no colons. -// --> strings/unquoted_strings.baml:4 -// | -// 3 | options { -// 4 | thing hello'world -// 5 | banned @helloworld -// | -// error: Error validating Client "Hello": This field declaration is invalid. It is either missing a name or a type. -// --> strings/unquoted_strings.baml:5 -// | -// 4 | thing hello'world -// 5 | banned @helloworld -// | -// error: Error validating Client "Hello": This field declaration is invalid. It is either missing a name or a type. -// --> strings/unquoted_strings.baml:6 -// | -// 5 | banned @helloworld -// 6 | banned2 #helloworld -// | -// error: Error validating: This line is not a valid field or attribute definition. A valid property may look like: 'myProperty "some value"' for example, with no colons. -// --> strings/unquoted_strings.baml:6 -// | -// 5 | banned @helloworld -// 6 | banned2 #helloworld -// 7 | banned3 hello(world) -// | -// error: Error validating: This line is not a valid field or attribute definition. A valid property may look like: 'myProperty "some value"' for example, with no colons. -// --> strings/unquoted_strings.baml:7 -// | -// 6 | banned2 #helloworld -// 7 | banned3 hello(world) -// 8 | } -// | // error: Error validating: This line is invalid. It does not start with any known Baml schema keyword. -// --> strings/unquoted_strings.baml:9 -// | -// 8 | } -// 9 | } -// 10 | -// | -// error: Error validating: Unknown field `thing` in client -// --> strings/unquoted_strings.baml:4 +// --> strings/unquoted_strings.baml:1 // | -// 3 | options { -// 4 | thing hello'world // | -// error: Error validating: Unknown field `banned3` in client -// --> strings/unquoted_strings.baml:7 -// | -// 6 | banned2 #helloworld -// 7 | banned3 hello(world) +// 1 | client Hello { +// 2 | provider baml-openai-chat // | diff --git a/engine/baml-runtime/tests/test_runtime.rs b/engine/baml-runtime/tests/test_runtime.rs index 51104544c9..b005de869f 100644 --- a/engine/baml-runtime/tests/test_runtime.rs +++ b/engine/baml-runtime/tests/test_runtime.rs @@ -109,8 +109,8 @@ mod internal_tests { None, None, None, - None, HashMap::new(), + None, TripWire::new(None), ) .await; From 427dd68ae6a247906269bcc8a79c92c3e7c86f8d Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 21 Oct 2025 20:32:33 -0700 Subject: [PATCH 15/16] tests --- engine/baml-compiler/src/builtin.rs | 8 +-- engine/baml-compiler/src/codegen.rs | 4 +- engine/baml-compiler/src/hir/lowering.rs | 67 ++++++++++++++----- engine/baml-compiler/src/thir/typecheck.rs | 13 +++- .../tests/validation_files/expr/null.baml | 20 ++++++ engine/baml-vm/tests/builtins.rs | 2 +- engine/baml-vm/tests/common.rs | 1 + engine/baml-vm/tests/watch.rs | 20 +++--- 8 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 engine/baml-lib/baml/tests/validation_files/expr/null.baml diff --git a/engine/baml-compiler/src/builtin.rs b/engine/baml-compiler/src/builtin.rs index 0567a16e58..4c3c33b378 100644 --- a/engine/baml-compiler/src/builtin.rs +++ b/engine/baml-compiler/src/builtin.rs @@ -78,7 +78,7 @@ pub fn builtin_enums() -> Vec { }] } -/// Create a type for the std::Request class +/// Create a type for the baml.Request class pub fn std_request_type() -> TypeIR { TypeIR::class(classes::REQUEST) } @@ -94,12 +94,10 @@ pub fn std_fetch_value_signature(return_type: TypeIR) -> TypeIR { } pub fn is_builtin_identifier(identifier: &str) -> bool { - identifier.starts_with("std::") - || identifier.starts_with("baml::") - || identifier.starts_with("std.") - || identifier.starts_with("baml.") + identifier.starts_with("baml.") || identifier == "true" || identifier == "false" + || identifier == "null" } pub fn is_builtin_class(class_name: &str) -> bool { diff --git a/engine/baml-compiler/src/codegen.rs b/engine/baml-compiler/src/codegen.rs index 23936e6565..088ca0506a 100644 --- a/engine/baml-compiler/src/codegen.rs +++ b/engine/baml-compiler/src/codegen.rs @@ -907,10 +907,10 @@ impl<'g> HirCompiler<'g> { self.emit(Instruction::Assert); } thir::Statement::WatchOptions { .. } => { - todo!("bytecode codegen update to variable's WatchOptions") + // todo!("bytecode codegen update to variable's WatchOptions") } thir::Statement::WatchNotify { .. } => { - todo!("bytecode codegen for manual notification trigger") + // todo!("bytecode codegen for manual notification trigger") } } } diff --git a/engine/baml-compiler/src/hir/lowering.rs b/engine/baml-compiler/src/hir/lowering.rs index 85928e7d80..eadf11e47b 100644 --- a/engine/baml-compiler/src/hir/lowering.rs +++ b/engine/baml-compiler/src/hir/lowering.rs @@ -178,33 +178,64 @@ pub fn type_ir_from_ast(type_: &ast::FieldType) -> TypeIR { streaming_behavior, }; - match type_ { - ast::FieldType::Symbol(_, name, _) => TypeIR::Class { - name: name.name().to_string(), - mode: baml_types::ir_type::StreamingMode::NonStreaming, - dynamic: false, - meta, - }, - ast::FieldType::Primitive(_, prim, _, _) => TypeIR::Primitive(*prim, meta), - ast::FieldType::List(_, inner, dims, _, _) => { + let base_type = match type_ { + ast::FieldType::Symbol(arity, name, _) => { + let base = TypeIR::Class { + name: name.name().to_string(), + mode: baml_types::ir_type::StreamingMode::NonStreaming, + dynamic: false, + meta, + }; + if arity.is_optional() { + TypeIR::optional(base) + } else { + base + } + } + ast::FieldType::Primitive(arity, prim, _, _) => { + let base = TypeIR::Primitive(*prim, meta); + if arity.is_optional() { + TypeIR::optional(base) + } else { + base + } + } + ast::FieldType::List(arity, inner, dims, _, _) => { // Respect multi-dimensional arrays (e.g., int[][] has dims=2) let mut lowered_inner = type_ir_from_ast(inner); for _ in 0..*dims { lowered_inner = TypeIR::List(Box::new(lowered_inner), meta.clone()); } - lowered_inner + if arity.is_optional() { + TypeIR::optional(lowered_inner) + } else { + lowered_inner + } + } + ast::FieldType::Map(arity, box_pair, _, _) => { + let base = TypeIR::Map( + Box::new(type_ir_from_ast(&box_pair.0)), + Box::new(type_ir_from_ast(&box_pair.1)), + meta, + ); + if arity.is_optional() { + TypeIR::optional(base) + } else { + base + } } - ast::FieldType::Map(_, box_pair, _, _) => TypeIR::Map( - Box::new(type_ir_from_ast(&box_pair.0)), - Box::new(type_ir_from_ast(&box_pair.1)), - meta, - ), - ast::FieldType::Union(_, types, _, _) => { + ast::FieldType::Union(arity, types, _, _) => { let union_types: Vec = types.iter().map(type_ir_from_ast).collect(); - TypeIR::union_with_meta(union_types, meta) + let base = TypeIR::union_with_meta(union_types, meta); + if arity.is_optional() { + TypeIR::optional(base) + } else { + base + } } _ => TypeIR::Primitive(TypeValue::String, meta), // Default case for other variants - } + }; + base_type } /// Is the type complex enough that it should be parenthesized if it's not diff --git a/engine/baml-compiler/src/thir/typecheck.rs b/engine/baml-compiler/src/thir/typecheck.rs index 9ce3c6fd75..e4ed9aa4c4 100644 --- a/engine/baml-compiler/src/thir/typecheck.rs +++ b/engine/baml-compiler/src/thir/typecheck.rs @@ -1262,7 +1262,7 @@ fn typecheck_assignment( let rhs_type = &rhs.meta().1; if let (Some(left_type), Some(val_type)) = (lhs.meta().1.as_ref(), rhs_type) { - if !types_compatible(left_type, val_type) { + if !val_type.is_subtype(left_type) { diagnostics.push_error(DatamodelError::new_validation_error( &format!( "Cannot assign {} to {}", @@ -1418,6 +1418,17 @@ pub fn typecheck_expression( BamlValueWithMeta::String(value.clone(), (span.clone(), Some(TypeIR::string()))), ), hir::Expression::Identifier(name, span) => { + // Special case for null literal + if name == "null" { + return thir::Expr::Value(BamlValueWithMeta::Null(( + span.clone(), + Some(TypeIR::Primitive( + baml_types::TypeValue::Null, + Default::default(), + )), + ))); + } + // Enum access: let x = Shape.Rectangle if let Some(enum_def) = context.enums.get(name) { return thir::Expr::Var( diff --git a/engine/baml-lib/baml/tests/validation_files/expr/null.baml b/engine/baml-lib/baml/tests/validation_files/expr/null.baml new file mode 100644 index 0000000000..97efbcd896 --- /dev/null +++ b/engine/baml-lib/baml/tests/validation_files/expr/null.baml @@ -0,0 +1,20 @@ +class Node { + value int + next Node? +} + +function main() -> int { + let a = Node { value: 1, next: null }; + let b = Node { value: 2, next: a }; + + // Create a circular reference + a.next = b; + + let copy = baml.deep_copy(a); + + // Modify the original + a.value = 99; + + // The copy should be unchanged + copy.value +} diff --git a/engine/baml-vm/tests/builtins.rs b/engine/baml-vm/tests/builtins.rs index 543a53f3d5..2fc9501de9 100644 --- a/engine/baml-vm/tests/builtins.rs +++ b/engine/baml-vm/tests/builtins.rs @@ -16,7 +16,7 @@ fn builtin_method_call() -> anyhow::Result<()> { } "#, function: "main", - expected: ExecState::Complete(Value::Int(4)), + expected: ExecState::Complete(Value::Int(3)), }) } diff --git a/engine/baml-vm/tests/common.rs b/engine/baml-vm/tests/common.rs index c5f3d244d8..186a4fac6b 100644 --- a/engine/baml-vm/tests/common.rs +++ b/engine/baml-vm/tests/common.rs @@ -238,6 +238,7 @@ pub fn assert_vm_fails_with_inspection( Ok(()) } +#[track_caller] pub fn assert_vm_executes(input: Program) -> anyhow::Result<()> { assert_vm_executes_with_inspection(input, |_vm| Ok(())) } diff --git a/engine/baml-vm/tests/watch.rs b/engine/baml-vm/tests/watch.rs index 4eff831855..036878f1b1 100644 --- a/engine/baml-vm/tests/watch.rs +++ b/engine/baml-vm/tests/watch.rs @@ -50,7 +50,7 @@ fn stop_notifying_on_scope_exit() -> anyhow::Result<()> { function scope_exit() -> Point { let outter_point = { - let point = Point { x: 0, y: 0 } @watch; + watch let point = Point { x: 0, y: 0 }; point.x = 1; // Expect only one notification here. point }; @@ -82,7 +82,7 @@ fn notify_on_function_call_modifications() -> anyhow::Result<()> { } function call_function() -> Point { - let point = Point { x: 0, y: 0 } @watch; + watch let point = Point { x: 0, y: 0 }; point.set(1, 2); point } @@ -105,7 +105,7 @@ fn notify_on_change_with_alias() -> anyhow::Result<()> { } function alias() -> Point { - let point = Point { x: 0, y: 0 } @watch; + watch let point = Point { x: 0, y: 0 }; let alias = point; alias.x = 1; // Notify @@ -128,7 +128,7 @@ fn notify_on_change_with_alias_in_nested_scope() -> anyhow::Result<()> { } function nested_alias() -> Point { - let point = Point { x: 0, y: 0 } @watch; + watch let point = Point { x: 0, y: 0 }; if (true) { let alias = point; alias.x = 1; // Notify @@ -161,10 +161,10 @@ fn notify_when_nested_object_is_modified_after_addtion() -> anyhow::Result<()> { } function nested_object_added() -> Vec2D { - let vec = Vec2D { + watch let vec = Vec2D { p: Point { x: Value { value: 0 }, y: Value { value: 0 } }, q: Point { x: Value { value: 0 }, y: Value { value: 0 } }, - } @watch; + }; let p = Point { x: Value { value: 1 }, y: Value { value: 1 } }; @@ -201,10 +201,10 @@ fn dont_notify_when_nested_object_is_modified_after_removal() -> anyhow::Result< } function nested_object_removed() -> Vec2D { - let vec = Vec2D { + watch let vec = Vec2D { p: Point { x: Value { value: 0 }, y: Value { value: 0 } }, q: Point { x: Value { value: 0 }, y: Value { value: 0 } }, - } @watch; + }; let p = vec.p; @@ -232,9 +232,9 @@ fn cyclic_graph() -> anyhow::Result<()> { function cycle() -> int { let v1 = Vertex { value: 1, edges: [] }; - let v2 = Vertex { value: 2, edges: [] } @watch; + watch let v2 = Vertex { value: 2, edges: [] }; let v3 = Vertex { value: 3, edges: [] }; - let v4 = Vertex { value: 4, edges: [] } @watch; + watch let v4 = Vertex { value: 4, edges: [] }; // NO EMIT (neither v2 nor v4 have changed) v1.edges = [v2]; From 01cb619ab44036e5ed69f208c603e8abfcba1abc Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Wed, 22 Oct 2025 13:03:23 -0700 Subject: [PATCH 16/16] diagnostics for parsing_catch_all --- engine/baml-lib/ast/src/parser/helpers.rs | 75 +- .../ast/src/parser/parse_arguments.rs | 2 +- .../ast/src/parser/parse_assignment.rs | 7 +- .../ast/src/parser/parse_attribute.rs | 12 +- .../baml-lib/ast/src/parser/parse_comments.rs | 16 +- engine/baml-lib/ast/src/parser/parse_expr.rs | 61 +- .../baml-lib/ast/src/parser/parse_expr.rs.bak | 1231 +++++++++++++++++ .../ast/src/parser/parse_expression.rs | 85 +- .../ast/src/parser/parse_expression.rs.bak | 829 +++++++++++ engine/baml-lib/ast/src/parser/parse_field.rs | 16 +- .../ast/src/parser/parse_identifier.rs | 21 +- .../ast/src/parser/parse_identifier.rs.bak | 92 ++ .../ast/src/parser/parse_named_args_list.rs | 7 +- .../ast/src/parser/parse_template_string.rs | 5 +- .../src/parser/parse_type_builder_block.rs | 15 +- .../src/parser/parse_type_expression_block.rs | 13 +- engine/baml-lib/ast/src/parser/parse_types.rs | 72 +- .../parser/parse_value_expression_block.rs | 6 +- ...workflow_emit.baml => workflow_watch.baml} | 0 ...simple.baml => workflow_watch_simple.baml} | 0 integ-tests/go/baml_client/baml_source_map.go | 4 +- .../python-v1/baml_client/inlinedbaml.py | 4 +- integ-tests/python/baml_client/inlinedbaml.py | 4 +- integ-tests/react/baml_client/inlinedbaml.ts | 4 +- .../typescript-esm/baml_client/inlinedbaml.ts | 4 +- .../typescript/baml_client/inlinedbaml.ts | 4 +- 26 files changed, 2405 insertions(+), 184 deletions(-) create mode 100644 engine/baml-lib/ast/src/parser/parse_expr.rs.bak create mode 100644 engine/baml-lib/ast/src/parser/parse_expression.rs.bak create mode 100644 engine/baml-lib/ast/src/parser/parse_identifier.rs.bak rename integ-tests/baml_src/test-files/workflows/{workflow_emit.baml => workflow_watch.baml} (100%) rename integ-tests/baml_src/test-files/workflows/{workflow_emit_simple.baml => workflow_watch_simple.baml} (100%) diff --git a/engine/baml-lib/ast/src/parser/helpers.rs b/engine/baml-lib/ast/src/parser/helpers.rs index 2251dcb52c..60338e752a 100644 --- a/engine/baml-lib/ast/src/parser/helpers.rs +++ b/engine/baml-lib/ast/src/parser/helpers.rs @@ -1,9 +1,11 @@ +use internal_baml_diagnostics::{DatamodelError, Diagnostics}; + use super::Rule; pub type Pair<'a> = pest::iterators::Pair<'a, Rule>; #[track_caller] -pub fn parsing_catch_all(token: Pair<'_>, kind: &str) { +pub fn parsing_catch_all(token: Pair<'_>, kind: &str, diagnostics: &mut Diagnostics) { match token.as_rule() { Rule::empty_lines | Rule::trailing_comment @@ -11,46 +13,47 @@ pub fn parsing_catch_all(token: Pair<'_>, kind: &str) { | Rule::block_comment | Rule::SPACER_TEXT | Rule::NEWLINE => {} - x => unreachable!( - "Encountered impossible {} during parsing: {:?} {:?}", - kind, - &x, - token.clone().tokens() - ), + x => { + let message = format!( + "Encountered impossible {} during parsing: {:?} {:?}", + kind, + &x, + token.clone().tokens() + ); + diagnostics.push_error(DatamodelError::new_parser_error( + message, + diagnostics.span(token.as_span()), + )) + } } } -#[macro_export] -macro_rules! assert_correct_parser { - ($pair:expr, $rule:expr) => { - assert_eq!( - $pair.as_rule(), - $rule, - "Expected {:?}. Got: {:?}.", - $rule, - $pair.as_rule() - ); - }; - ($pair:expr, $($rule:expr),+ ) => { - let rules = vec![$($rule),+]; - assert!( - rules.contains(&$pair.as_rule()), - "Expected one of {:?}. Got: {:?}.", - rules, - $pair.as_rule() - ); - }; +#[track_caller] +pub fn assert_correct_parser(pair: &Pair<'_>, expected: &[Rule], diagnostics: &mut Diagnostics) { + if !expected.contains(&pair.as_rule()) { + let message = if expected.len() == 1 { + format!("Expected {:?}. Got: {:?}.", expected[0], pair.as_rule()) + } else { + format!("Expected one of {:?}. Got: {:?}.", expected, pair.as_rule()) + }; + diagnostics.push_error(DatamodelError::new_parser_error( + message, + diagnostics.span(pair.as_span()), + )); + } } -#[macro_export] -macro_rules! unreachable_rule { - ($pair:expr, $rule:expr) => { - unreachable!( - "Encountered impossible field during parsing {:?}: {:?}", - $rule, - $pair.as_rule() - ) - }; +#[track_caller] +pub fn unreachable_rule(pair: &Pair<'_>, context: &str, diagnostics: &mut Diagnostics) { + let message = format!( + "Encountered impossible field during parsing {:?}: {:?}", + context, + pair.as_rule() + ); + diagnostics.push_error(DatamodelError::new_parser_error( + message, + diagnostics.span(pair.as_span()), + )); } #[macro_export] diff --git a/engine/baml-lib/ast/src/parser/parse_arguments.rs b/engine/baml-lib/ast/src/parser/parse_arguments.rs index 48d670c000..69facf39b6 100644 --- a/engine/baml-lib/ast/src/parser/parse_arguments.rs +++ b/engine/baml-lib/ast/src/parser/parse_arguments.rs @@ -35,7 +35,7 @@ pub(crate) fn parse_arguments_list( }); } } - _ => parsing_catch_all(current, "attribute arguments"), + _ => parsing_catch_all(current, "attribute arguments", diagnostics), } } } diff --git a/engine/baml-lib/ast/src/parser/parse_assignment.rs b/engine/baml-lib/ast/src/parser/parse_assignment.rs index bf172a2a52..551068af97 100644 --- a/engine/baml-lib/ast/src/parser/parse_assignment.rs +++ b/engine/baml-lib/ast/src/parser/parse_assignment.rs @@ -1,12 +1,11 @@ use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, Pair}, parse_identifier::parse_identifier, Rule, }; use crate::{ - assert_correct_parser, ast::*, parser::{parse_field::parse_field_type_with_attr, parse_types::parse_field_type}, }; @@ -16,7 +15,7 @@ use crate::{ /// It only works with type aliases for now, it's not generic over all /// expressions. pub(crate) fn parse_assignment(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Assignment { - assert_correct_parser!(pair, Rule::type_alias); + assert_correct_parser(&pair, &[Rule::type_alias], diagnostics); let span = pair.as_span(); @@ -56,7 +55,7 @@ pub(crate) fn parse_assignment(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> field_type = parse_field_type_with_attr(current, false, diagnostics) } - _ => parsing_catch_all(current, "type_alias"), + _ => parsing_catch_all(current, "type_alias", diagnostics), } } diff --git a/engine/baml-lib/ast/src/parser/parse_attribute.rs b/engine/baml-lib/ast/src/parser/parse_attribute.rs index 41f49bb4b1..c0c19b75be 100644 --- a/engine/baml-lib/ast/src/parser/parse_attribute.rs +++ b/engine/baml-lib/ast/src/parser/parse_attribute.rs @@ -2,18 +2,22 @@ use baml_types::ConstraintLevel; use internal_baml_diagnostics::DatamodelError; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, Pair}, parse_identifier::{parse_identifier, parse_path_identifier}, Rule, }; -use crate::{assert_correct_parser, ast::*, parser::parse_arguments::parse_arguments_list}; +use crate::{ast::*, parser::parse_arguments::parse_arguments_list}; pub(crate) fn parse_attribute( pair: Pair<'_>, parenthesized: bool, diagnostics: &mut internal_baml_diagnostics::Diagnostics, ) -> Attribute { - assert_correct_parser!(pair, Rule::block_attribute, Rule::field_attribute); + assert_correct_parser( + &pair, + &[Rule::block_attribute, Rule::field_attribute], + diagnostics, + ); let span = diagnostics.span(pair.as_span()); let mut name = None; @@ -26,7 +30,7 @@ pub(crate) fn parse_attribute( Rule::arguments_list => { parse_arguments_list(current, &mut arguments, &name, diagnostics) } - _ => parsing_catch_all(current, "attribute"), + _ => parsing_catch_all(current, "attribute", diagnostics), } } diff --git a/engine/baml-lib/ast/src/parser/parse_comments.rs b/engine/baml-lib/ast/src/parser/parse_comments.rs index f193239dfc..d99535dd49 100644 --- a/engine/baml-lib/ast/src/parser/parse_comments.rs +++ b/engine/baml-lib/ast/src/parser/parse_comments.rs @@ -1,17 +1,22 @@ +use internal_baml_diagnostics::Diagnostics; + use super::{ helpers::{parsing_catch_all, Pair}, Rule, }; use crate::ast::Comment; -pub(crate) fn parse_comment_block(token: Pair<'_>) -> Option { +pub(crate) fn parse_comment_block( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { debug_assert!(token.as_rule() == Rule::comment_block); let mut lines = Vec::new(); for comment in token.clone().into_inner() { match comment.as_rule() { Rule::doc_comment => lines.push(parse_doc_comment(comment)), Rule::comment | Rule::NEWLINE | Rule::WHITESPACE => {} - _ => parsing_catch_all(comment, "comment block"), + _ => parsing_catch_all(comment, "comment block", diagnostics), } } @@ -24,14 +29,17 @@ pub(crate) fn parse_comment_block(token: Pair<'_>) -> Option { } } -pub(crate) fn parse_trailing_comment(pair: Pair<'_>) -> Option { +pub(crate) fn parse_trailing_comment( + pair: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { debug_assert_eq!(pair.as_rule(), Rule::trailing_comment); let mut lines = Vec::new(); for current in pair.into_inner() { match current.as_rule() { Rule::doc_comment => lines.push(parse_doc_comment(current)), Rule::comment | Rule::NEWLINE | Rule::WHITESPACE => {} - _ => parsing_catch_all(current, "trailing comment"), + _ => parsing_catch_all(current, "trailing comment", diagnostics), } } diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs b/engine/baml-lib/ast/src/parser/parse_expr.rs index 7a0eb68e7b..22c27e8f53 100644 --- a/engine/baml-lib/ast/src/parser/parse_expr.rs +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs @@ -3,12 +3,11 @@ use std::collections::HashMap; use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, unreachable_rule, Pair}, parse_identifier::parse_identifier, Rule, }; use crate::{ - assert_correct_parser, ast::{ self, expr::ExprFn, App, ArgumentsList, AssignOp, AssignOpStmt, AssignStmt, ExprStmt, Expression, ExpressionBlock, ForLoopStmt, LetStmt, Stmt, TopLevelAssignment, *, @@ -18,11 +17,10 @@ use crate::{ parse_field::parse_field_type_chain, parse_identifier, parse_named_args_list::parse_named_argument_list, parse_types::parse_field_type, }, - unreachable_rule, }; pub fn parse_expr_fn(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::expr_fn); + assert_correct_parser(&token, &[Rule::expr_fn], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); let name = parse_identifier(tokens.next()?, diagnostics); @@ -89,7 +87,7 @@ pub fn parse_top_level_assignment( token: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Option { - assert_correct_parser!(token, Rule::top_level_assignment); + assert_correct_parser(&token, &[Rule::top_level_assignment], diagnostics); let mut tokens = token.into_inner(); let only_let_stmt = |name, span, diagnostics: &mut Diagnostics| { @@ -139,7 +137,7 @@ pub fn parse_top_level_assignment( } fn parse_while_loop(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::while_loop); + assert_correct_parser(&pair, &[Rule::while_loop], diagnostics); let span = diagnostics.span(pair.as_span()); let mut while_loop = pair.into_inner(); @@ -159,7 +157,7 @@ fn parse_while_loop(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::for_loop); + assert_correct_parser(&token, &[Rule::for_loop], diagnostics); let span = diagnostics.span(token.as_span()); @@ -177,7 +175,10 @@ fn parse_for_loop(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { parse_iterator_for_loop(in_between_rule, body, span, diagnostics).map(Stmt::ForLoop) } - _ => unreachable_rule!(in_between_rule, Rule::for_loop), + _ => { + unreachable_rule(&in_between_rule, "for_loop", diagnostics); + None + } } } @@ -187,7 +188,7 @@ fn parse_c_for_loop( span: Span, diagnostics: &mut Diagnostics, ) -> Option { - assert_correct_parser!(token, Rule::c_for_loop); + assert_correct_parser(&token, &[Rule::c_for_loop], diagnostics); let mut header = token.into_inner(); @@ -253,7 +254,7 @@ fn parse_iterator_for_loop( span: Span, diagnostics: &mut Diagnostics, ) -> Option { - assert_correct_parser!(token, Rule::iterator_for_loop); + assert_correct_parser(&token, &[Rule::iterator_for_loop], diagnostics); let mut header = token.into_inner(); @@ -282,7 +283,7 @@ fn parse_block_aware_tail_expression( pair: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Option { - assert_correct_parser!(pair, Rule::block_aware_tail_expression); + assert_correct_parser(&pair, &[Rule::block_aware_tail_expression], diagnostics); let inner = pair .into_inner() @@ -292,7 +293,10 @@ fn parse_block_aware_tail_expression( match inner.as_rule() { Rule::expression => parse_expression(inner, diagnostics), Rule::identifier => Some(Expression::Identifier(parse_identifier(inner, diagnostics))), - _ => unreachable_rule!(inner, Rule::block_aware_tail_expression), + _ => { + unreachable_rule(&inner, "block_aware_tail_expression", diagnostics); + None + } } } @@ -375,7 +379,7 @@ pub fn consume_span_if_rule( } pub fn parse_top_level_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::top_level_stmt); + assert_correct_parser(&token, &[Rule::top_level_stmt], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -405,7 +409,7 @@ pub fn parse_top_level_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) } pub fn parse_expr_body_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::expr_body_stmt); + assert_correct_parser(&token, &[Rule::expr_body_stmt], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -435,7 +439,7 @@ pub fn parse_expr_body_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) } pub fn parse_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::stmt); + assert_correct_parser(&token, &[Rule::stmt], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -660,7 +664,10 @@ fn finish_assign_op_stmt( Rule::BIT_XOR_ASSIGN => AssignOp::BitXorAssign, Rule::BIT_SHL_ASSIGN => AssignOp::ShlAssign, Rule::BIT_SHR_ASSIGN => AssignOp::ShrAssign, - other => unreachable_rule!(op_token, other), + _ => { + unreachable_rule(&op_token, "assign_op", diagnostics); + AssignOp::AddAssign // Default fallback + } }; maybe_body.map(|body| AssignOpStmt { @@ -694,7 +701,7 @@ fn parse_assignment_expr( } pub fn parse_expr_block(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::expr_block); + assert_correct_parser(&token, &[Rule::expr_block], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); let mut stmts = Vec::new(); @@ -1058,7 +1065,7 @@ fn bind_headers_to_statement( } pub(crate) fn parse_fn_args(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Vec { - assert_correct_parser!(token, Rule::fn_args); + assert_correct_parser(&token, &[Rule::fn_args], diagnostics); token .into_inner() @@ -1067,7 +1074,7 @@ pub(crate) fn parse_fn_args(token: Pair<'_>, diagnostics: &mut Diagnostics) -> V } pub fn parse_fn_app(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::fn_app); + assert_correct_parser(&token, &[Rule::fn_app], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -1089,7 +1096,7 @@ pub fn parse_fn_app(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::generic_fn_app); + assert_correct_parser(&token, &[Rule::generic_fn_app], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -1115,7 +1122,7 @@ pub fn parse_generic_fn_app(token: Pair<'_>, diagnostics: &mut Diagnostics) -> O } pub fn parse_lambda(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::lambda); + assert_correct_parser(&token, &[Rule::lambda], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); let mut args = ArgumentsList { @@ -1134,7 +1141,7 @@ pub fn parse_function_body( } pub fn parse_if_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(token, Rule::if_expression); + assert_correct_parser(&token, &[Rule::if_expression], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -1150,15 +1157,17 @@ pub fn parse_if_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Op let else_branch = tokens.next().and_then(|else_branch_expr| { let else_branch_span = diagnostics.span(else_branch_expr.as_span()); - let else_branch = match else_branch_expr.as_rule() { + match else_branch_expr.as_rule() { Rule::expr_block => parse_expr_block(else_branch_expr, diagnostics) .map(|e| Box::new(Expression::ExprBlock(e, else_branch_span))), Rule::if_expression => parse_if_expression(else_branch_expr, diagnostics).map(Box::new), - _ => unreachable_rule!(else_branch_expr, Rule::if_expression), - }; - else_branch + _ => { + unreachable_rule(&else_branch_expr, "if_expression", diagnostics); + None + } + } }); Some(Expression::If( diff --git a/engine/baml-lib/ast/src/parser/parse_expr.rs.bak b/engine/baml-lib/ast/src/parser/parse_expr.rs.bak new file mode 100644 index 0000000000..f0dc9d143e --- /dev/null +++ b/engine/baml-lib/ast/src/parser/parse_expr.rs.bak @@ -0,0 +1,1231 @@ +use std::collections::HashMap; + +use internal_baml_diagnostics::{DatamodelError, Diagnostics}; + +use super::{ + helpers::{assert_correct_parser, parsing_catch_all, unreachable_rule, Pair}, + parse_identifier::parse_identifier, + Rule, +}; +use crate::{ + ast::{ + self, expr::ExprFn, App, ArgumentsList, AssignOp, AssignOpStmt, AssignStmt, ExprStmt, + Expression, ExpressionBlock, ForLoopStmt, LetStmt, Stmt, TopLevelAssignment, WatchArgument, + WatchDecorator, *, + }, + parser::{ + parse_arguments::parse_arguments_list, parse_expression::parse_expression, + parse_field::parse_field_type_chain, parse_identifier, + parse_named_args_list::parse_named_argument_list, parse_types::parse_field_type, + }, +}; + +pub fn parse_expr_fn(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser(&token, &[Rule::expr_fn], diagnostics); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + let name = parse_identifier(tokens.next()?, diagnostics); + let args = parse_named_argument_list(tokens.next()?, diagnostics); + let arrow_or_body = tokens.next()?; + + // We may or may not have an arrow and a return type. + // If the args list is immediately followed by an arrow, we have an arrow and a return type. + // Otherwise, we have just a body. + let (maybe_return_type, maybe_body) = if matches!(arrow_or_body.as_rule(), Rule::ARROW) { + let return_type = parse_field_type_chain(tokens.next()?, diagnostics); + let function_body = parse_function_body(tokens.next()?, diagnostics); + (Some(return_type), function_body) + } else { + diagnostics.push_error(DatamodelError::new_static( + "function must have a return type: e.g. function Foo() -> int", + span.clone(), + )); + let function_body = parse_function_body(arrow_or_body, diagnostics); + (None, function_body) + }; + match (maybe_return_type, maybe_body) { + (Some(return_type), Some(body)) => Some(ExprFn { + name, + args, + return_type, + body, + span, + annotations: vec![], + }), + // Even if the return type is missing, still create the ExprFn to prevent fallback to LLM function parsing + (None, Some(body)) => { + // Create a dummy return type to allow parsing to continue + use crate::ast::{FieldArity, FieldType, Identifier, Span}; + let dummy_return_type = FieldType::Symbol( + FieldArity::Required, + Identifier::Local("UnknownType".to_string(), Span::fake()), + None, + ); + Some(ExprFn { + name, + args, + return_type: Some(dummy_return_type), + body, + span, + annotations: vec![], + }) + } + _ => None, + } +} + +pub fn parse_top_level_assignment( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { + assert_correct_parser(&token, &[Rule::top_level_assignment], diagnostics); + let mut tokens = token.into_inner(); + + let only_let_stmt = |name, span, diagnostics: &mut Diagnostics| { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("{name} are not allowed at top level, only let statements are allowed"), + span, + )); + + None + }; + + let stmt_token = tokens.next()?; + let parsed_stmt = if stmt_token.as_rule() == Rule::top_level_stmt { + parse_top_level_statement(stmt_token, diagnostics) + } else { + parse_statement(stmt_token, diagnostics) + }; + + match parsed_stmt? { + Stmt::Let(stmt) => Some(TopLevelAssignment { stmt }), + Stmt::Assign(stmt) => only_let_stmt("assignments", stmt.span, diagnostics), + Stmt::AssignOp(stmt) => only_let_stmt("assignments", stmt.span, diagnostics), + Stmt::ForLoop(ForLoopStmt { span, .. }) | Stmt::CForLoop(CForLoopStmt { span, .. }) => { + only_let_stmt("for loops", span, diagnostics) + } + Stmt::Expression(expr) => only_let_stmt("expressions", expr.span.clone(), diagnostics), + Stmt::Semicolon(expr) => { + only_let_stmt("semicolon expressions", expr.span().clone(), diagnostics) + } + Stmt::WhileLoop(stmt) => only_let_stmt("while loops", stmt.span, diagnostics), + Stmt::Break(span) => only_let_stmt("break statements", span, diagnostics), + Stmt::Continue(span) => only_let_stmt("continue statements", span, diagnostics), + Stmt::Return(ReturnStmt { span, .. }) => { + only_let_stmt("return statements", span, diagnostics) + } + + Stmt::Assert(AssertStmt { span, .. }) => { + only_let_stmt("assert statements", span, diagnostics) + } + } +} + +fn parse_while_loop(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser(&pair, &[Rule::while_loop], diagnostics); + + let span = diagnostics.span(pair.as_span()); + let mut while_loop = pair.into_inner(); + + let condition_rule = + check_parentheses_around_rule(&mut while_loop, diagnostics, "while loop condition")?; + + let condition = parse_block_aware_tail_expression(condition_rule, diagnostics)?; + + let body = parse_expr_block(while_loop.next()?, diagnostics)?; + + Some(Stmt::WhileLoop(WhileStmt { + condition, + body, + span, + })) +} + +fn parse_for_loop(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::for_loop); + + let span = diagnostics.span(token.as_span()); + + let mut tokens = token.into_inner(); + + let in_between_rule = + check_parentheses_around_rule(&mut tokens, diagnostics, "for loop header")?; + + let body = parse_expr_block(tokens.next()?, diagnostics)?; + + match in_between_rule.as_rule() { + Rule::c_for_loop => { + parse_c_for_loop(in_between_rule, body, span, diagnostics).map(Stmt::CForLoop) + } + Rule::iterator_for_loop => { + parse_iterator_for_loop(in_between_rule, body, span, diagnostics).map(Stmt::ForLoop) + } + _ => unreachable_rule!(in_between_rule, Rule::for_loop), + } +} + +fn parse_c_for_loop( + token: Pair<'_>, + body: ExpressionBlock, + span: Span, + diagnostics: &mut Diagnostics, +) -> Option { + assert_correct_parser!(token, Rule::c_for_loop); + + let mut header = token.into_inner(); + + let init_stmt = consume_if_rule(&mut header, Rule::c_for_init_stmt).map(|rule| { + rule.into_inner() + .next() + .expect("c_for_init_stmt cannot accept empty input") + }); + let condition = consume_if_rule(&mut header, Rule::expression); + let after_stmt = consume_if_rule(&mut header, Rule::c_for_after_stmt).map(|rule| { + rule.into_inner() + .next() + .expect("c_for_after_stmt cannot accept empty input") + }); + + let init_stmt = parse_optional_rule(init_stmt, |rule| { + let span = diagnostics.span(rule.as_span()); + parse_statement_inner_rule(rule, span, diagnostics) + })? + .map(Box::new); + + let condition = parse_optional_rule(condition, |rule| parse_expression(rule, diagnostics))?; + + let after_stmt = parse_optional_rule(after_stmt, |rule| { + let span = diagnostics.span(rule.as_span()); + + match rule.as_rule() { + Rule::block_aware_assign_stmt => { + let mut tokens = rule.into_inner(); + + let left = parse_expression(tokens.next()?, diagnostics)?; + + let expr = parse_block_aware_tail_expression(tokens.next()?, diagnostics)?; + + Some(Stmt::Assign(AssignStmt { left, expr, span })) + } + Rule::block_aware_assign_op_stmt => { + let mut tokens = rule.into_inner(); + + let left = tokens.next()?; + let op = tokens.next()?; + let expr = parse_block_aware_tail_expression(tokens.next()?, diagnostics); + + finish_assign_op_stmt(span, diagnostics, left, op, expr).map(Stmt::AssignOp) + } + _ => parse_statement_inner_rule(rule, span, diagnostics), + } + })? + .map(Box::new); + + Some(CForLoopStmt { + init_stmt, + condition, + after_stmt, + body, + span, + }) +} + +fn parse_iterator_for_loop( + token: Pair<'_>, + body: ExpressionBlock, + span: Span, + diagnostics: &mut Diagnostics, +) -> Option { + assert_correct_parser!(token, Rule::iterator_for_loop); + + let mut header = token.into_inner(); + + // Support optional `let` before the identifier + let first = header.next()?; + let (has_let, ident_pair) = if first.as_rule() == Rule::LET_KEYWORD { + (true, header.next()?) + } else { + (false, first) + }; + + let identifier = parse_identifier(ident_pair, diagnostics); + let iterator = parse_block_aware_tail_expression(header.next()?, diagnostics)?; + + Some(ForLoopStmt { + identifier, + iterator, + body, + span, + has_let, + annotations: vec![], + }) +} + +fn parse_block_aware_tail_expression( + pair: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { + assert_correct_parser!(pair, Rule::block_aware_tail_expression); + + let inner = pair + .into_inner() + .next() + .expect("block aware tail expression is not empty"); + + match inner.as_rule() { + Rule::expression => parse_expression(inner, diagnostics), + Rule::identifier => Some(Expression::Identifier(parse_identifier(inner, diagnostics))), + _ => unreachable_rule!(inner, Rule::block_aware_tail_expression), + } +} + +/// Lifts the error from `parse` into the top-level optional. The second level optional will +/// reflect whether there was a rule in the first place. +fn parse_optional_rule( + rule: Option>, + parse: impl FnOnce(Pair<'_>) -> Option, +) -> Option> { + rule.map_or(Some(None), |rule| parse(rule).map(Some)) +} + +fn check_parentheses_around_rule<'src>( + tokens: &mut pest::iterators::Pairs<'src, Rule>, + diagnostics: &mut Diagnostics, + construct_name: &'static str, +) -> Option> { + let lparen_span = consume_span_if_rule(tokens, diagnostics, Rule::openParen); + + let in_between_rule = tokens.next()?; + + let rparen_span = consume_span_if_rule(tokens, diagnostics, Rule::closeParen); + + let in_between_span = diagnostics.span(in_between_rule.as_span()); + + check_parentheses_around( + diagnostics, + construct_name, + lparen_span, + rparen_span, + in_between_span, + ); + + Some(in_between_rule) +} + +/// Emits diagnostics depending on what parentheses spans are `None`. +fn check_parentheses_around( + diagnostics: &mut Diagnostics, + construct_name: &'static str, + lparen_span: Option, + rparen_span: Option, + in_between_span: Span, +) { + match (lparen_span, rparen_span) { + (None, None) => diagnostics.push_error(DatamodelError::new_validation_error( + &format!("expected parentheses around {construct_name}"), + in_between_span, + )), + (None, Some(r)) => diagnostics.push_error(DatamodelError::new_validation_error( + "expected opening parentheses for this closing parentheses", + r, + )), + (Some(l), None) => diagnostics.push_error(DatamodelError::new_validation_error( + "expected closing parentheses for this opening parentheses", + l, + )), + // both present. Nothing to check. + (Some(_), Some(_)) => {} + } +} + +pub fn consume_if_rule<'src>( + tokens: &mut pest::iterators::Pairs<'src, Rule>, + rule: Rule, +) -> Option> { + if tokens.peek().is_some_and(|x| x.as_rule() == rule) { + Some(tokens.next().unwrap()) + } else { + None + } +} + +pub fn consume_span_if_rule( + tokens: &mut pest::iterators::Pairs<'_, Rule>, + diagnostics: &Diagnostics, + rule: Rule, +) -> Option { + consume_if_rule(tokens, rule).map(|rule| diagnostics.span(rule.as_span())) +} + +pub fn parse_top_level_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::top_level_stmt); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + let mut stmt = parse_statement_inner_rule(tokens.next()?, span.clone(), diagnostics); + + match tokens.next() { + Some(maybe_semicolon) if maybe_semicolon.as_str() == ";" => { + if let Some(Stmt::Expression(es)) = stmt { + stmt = Some(Stmt::Semicolon(es.expr)); + } + } + _ => { + // For top_level_stmt, emit semicolon error but don't fail parsing + if matches!( + stmt, + Some(Stmt::Let(_) | Stmt::Assign(_) | Stmt::AssignOp(_)) + ) { + diagnostics.push_error(DatamodelError::new_static( + "Statement must end with a semicolon.", + span, + )); + } + } + } + + stmt +} + +pub fn parse_expr_body_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::expr_body_stmt); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + let mut stmt = parse_statement_inner_rule(tokens.next()?, span.clone(), diagnostics); + + match tokens.next() { + Some(maybe_semicolon) if maybe_semicolon.as_str() == ";" => { + if let Some(Stmt::Expression(es)) = stmt { + stmt = Some(Stmt::Semicolon(es.expr)); + } + } + _ => { + // For expr_body_stmt, emit semicolon error but don't fail parsing + if matches!( + stmt, + Some(Stmt::Let(_) | Stmt::Assign(_) | Stmt::AssignOp(_)) + ) { + diagnostics.push_error(DatamodelError::new_static( + "Statement must end with a semicolon.", + span, + )); + } + } + } + + stmt +} + +pub fn parse_statement(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::stmt); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + let mut stmt = parse_statement_inner_rule(tokens.next()?, span.clone(), diagnostics); + + match tokens.next() { + Some(maybe_semicolon) if maybe_semicolon.as_str() == ";" => { + if let Some(Stmt::Expression(es)) = stmt { + stmt = Some(Stmt::Semicolon(es.expr)); + } + } + _ => { + if matches!( + stmt, + Some(Stmt::Let(_) | Stmt::Assign(_) | Stmt::AssignOp(_)) + ) { + diagnostics.push_error(DatamodelError::new_static( + "Statement must end with a semicolon.", + span, + )); + // Don't set stmt to None - keep the parsed statement even with the error + } + } + } + + stmt +} + +fn parse_statement_inner_rule( + stmt_token: Pair<'_>, + span: Span, + diagnostics: &mut Diagnostics, +) -> Option { + match stmt_token.as_rule() { + Rule::INVALID_STMT_STARTING_CHAR => { + diagnostics.push_error(DatamodelError::new_static("Invalid statement", span)); + None + } + Rule::assert_stmt => { + let assert_value = stmt_token.into_inner().next()?; + let value = parse_expression(assert_value, diagnostics)?; + + Some(Stmt::Assert(AssertStmt { value, span })) + } + + Rule::return_stmt => { + let return_value = stmt_token.into_inner().next()?; + let value = parse_expression(return_value, diagnostics)?; + + Some(Stmt::Return(ReturnStmt { value, span })) + } + Rule::assign_stmt => { + let mut assignment_tokens = stmt_token.into_inner(); + + let lhs = parse_expression(assignment_tokens.next()?, diagnostics)?; + + let rhs = assignment_tokens.next()?; + let rhs_span = diagnostics.span(rhs.as_span()); + let maybe_body = parse_assignment_expr(diagnostics, rhs, rhs_span); + maybe_body.map(|body| { + Stmt::Assign(AssignStmt { + left: lhs, + expr: body, + span, + }) + }) + } + Rule::assign_op_stmt => { + let mut assignment_tokens = stmt_token.into_inner(); + + let lhs = assignment_tokens.next()?; + let op_token = assignment_tokens.next()?; + let rhs = assignment_tokens.next()?; + + let rhs_span = diagnostics.span(rhs.as_span()); + let maybe_body = parse_assignment_expr(diagnostics, rhs, rhs_span); + + finish_assign_op_stmt(span, diagnostics, lhs, op_token, maybe_body).map(Stmt::AssignOp) + } + Rule::let_expr => { + let mut let_binding_tokens = stmt_token.into_inner(); + + let is_mutable = true; // Always mutable now after mut keyword removal + + let identifier = parse_identifier(let_binding_tokens.next()?, diagnostics); + + // Optional type annotation: `: ` + // Grammar packs this as a `let_type_annotation` pair if present. + let mut annotation = None; + let next_pair = let_binding_tokens.next()?; + let rhs_pair = if next_pair.as_rule() == Rule::let_type_annotation { + // Parse annotation's inner field_type_chain (skip the COLON token) + let ann_inner = next_pair.clone().into_inner(); + for inner in ann_inner { + if inner.as_rule() == Rule::field_type_chain { + annotation = super::parse_field::parse_field_type_chain(inner, diagnostics); + break; + } + } + // The next token must be the RHS expression. + let_binding_tokens.next()? + } else { + next_pair + }; + + let rhs_span = diagnostics.span(rhs_pair.as_span()); + let maybe_body = parse_assignment_expr(diagnostics, rhs_pair, rhs_span); + let mut watch = None; + if let Some(trailing) = let_binding_tokens.next() { + match trailing.as_rule() { + Rule::watch_decorator => { + watch = parse_watch_decorator(trailing, diagnostics); + } + _ => parsing_catch_all(trailing, "let expression", diagnostics), + } + } + + maybe_body.map(|body| { + Stmt::Let(LetStmt { + identifier, + is_mutable, + annotation, + expr: body, + span: span.clone(), + annotations: vec![], + watch, + }) + }) + } + Rule::BREAK_KEYWORD => Some(Stmt::Break(diagnostics.span(stmt_token.as_span()))), + Rule::CONTINUE_KEYWORD => Some(Stmt::Continue(diagnostics.span(stmt_token.as_span()))), + Rule::while_loop => parse_while_loop(stmt_token, diagnostics), + Rule::for_loop => parse_for_loop(stmt_token, diagnostics), + Rule::if_expression => parse_if_expression(stmt_token, diagnostics).map(|expr| { + Stmt::Expression(ExprStmt { + expr, + annotations: vec![], + span: span.clone(), + }) + }), + Rule::fn_app => parse_fn_app(stmt_token, diagnostics).map(|expr| { + Stmt::Expression(ExprStmt { + expr, + annotations: vec![], + span: span.clone(), + }) + }), + Rule::generic_fn_app => parse_generic_fn_app(stmt_token, diagnostics).map(|expr| { + Stmt::Expression(ExprStmt { + expr, + annotations: vec![], + span: span.clone(), + }) + }), + Rule::expression => parse_expression(stmt_token, diagnostics).map(|expr| { + Stmt::Expression(ExprStmt { + expr, + annotations: vec![], + span: span.clone(), + }) + }), + Rule::expr_block => parse_expr_block(stmt_token, diagnostics).map(|expr_block| { + Stmt::Expression(ExprStmt { + expr: Expression::ExprBlock(expr_block, span.clone()), + annotations: vec![], + span: span.clone(), + }) + }), + _ => { + diagnostics.push_error(DatamodelError::new_static("Expected statement", span)); + None + } + } +} + +/// Given identifier & operator rules, allows different parse strategies for the +/// rvalue expression. +fn finish_assign_op_stmt( + span: Span, + diagnostics: &mut Diagnostics, + lhs_rule: Pair<'_>, + op_token: Pair<'_>, + maybe_body: Option, +) -> Option { + let left = parse_expression(lhs_rule, diagnostics)?; + + let assign_op = match op_token.as_rule() { + Rule::ADD_ASSIGN => AssignOp::AddAssign, + Rule::SUB_ASSIGN => AssignOp::SubAssign, + Rule::MUL_ASSIGN => AssignOp::MulAssign, + Rule::DIV_ASSIGN => AssignOp::DivAssign, + Rule::MOD_ASSIGN => AssignOp::ModAssign, + Rule::BIT_AND_ASSIGN => AssignOp::BitAndAssign, + Rule::BIT_OR_ASSIGN => AssignOp::BitOrAssign, + Rule::BIT_XOR_ASSIGN => AssignOp::BitXorAssign, + Rule::BIT_SHL_ASSIGN => AssignOp::ShlAssign, + Rule::BIT_SHR_ASSIGN => AssignOp::ShrAssign, + other => unreachable_rule!(op_token, other), + }; + + maybe_body.map(|body| AssignOpStmt { + left, + assign_op, + expr: body, + span, + }) +} + +fn parse_assignment_expr( + diagnostics: &mut Diagnostics, + rhs: Pair<'_>, + rhs_span: Span, +) -> Option { + match rhs.as_rule() { + Rule::expr_block => { + let block_span = diagnostics.span(rhs.as_span()); + let maybe_expr_block = parse_expr_block(rhs, diagnostics); + maybe_expr_block.map(|expr_block| Expression::ExprBlock(expr_block, block_span)) + } + Rule::expression => parse_expression(rhs, diagnostics), + _ => { + diagnostics.push_error(DatamodelError::new_static( + "Parser only allows expr_block and expr here", + rhs_span, + )); + None + } + } +} + +fn parse_watch_decorator(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::watch_decorator); + let span = diagnostics.span(token.as_span()); + let mut decorator = WatchDecorator { + arguments: Vec::new(), + span, + }; + + for inner in token.into_inner() { + match inner.as_rule() { + Rule::watch_arguments => { + parse_watch_arguments(inner, diagnostics, &mut decorator.arguments) + } + Rule::SPACER_TEXT => {} + _ => parsing_catch_all(inner, "watch decorator", diagnostics), + } + } + + Some(decorator) +} + +fn parse_watch_arguments( + token: Pair<'_>, + diagnostics: &mut Diagnostics, + arguments: &mut Vec, +) { + assert_correct_parser!(token, Rule::watch_arguments); + for inner in token.into_inner() { + match inner.as_rule() { + Rule::watch_argument_kv => { + if let Some(argument) = parse_watch_argument(inner, diagnostics) { + arguments.push(argument); + } + } + Rule::watch_argument_invalid => { + let span = diagnostics.span(inner.as_span()); + diagnostics.push_error(DatamodelError::new_validation_error( + "@watch options must use `name=value` syntax (e.g. `name=updates`).", + span, + )); + + // Consume the invalid expression to keep parser state consistent. + for expr in inner.into_inner() { + if expr.as_rule() == Rule::expression { + let _ = parse_expression(expr, diagnostics); + } + } + } + Rule::watch_argument_missing_value => { + let span = diagnostics.span(inner.as_span()); + diagnostics.push_error(DatamodelError::new_validation_error( + "@watch options must provide a value after `=` (e.g. `name=updates`).", + span, + )); + } + Rule::SPACER_TEXT => {} + _ => parsing_catch_all(inner, "watch decorator arguments", diagnostics), + } + } +} + +fn parse_watch_argument(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::watch_argument_kv); + let span = diagnostics.span(token.as_span()); + let mut inner = token.into_inner(); + + let name_pair = inner.next()?; + let name = parse_identifier(name_pair, diagnostics); + let maybe_value_pair = inner.next(); + if maybe_value_pair.is_none() { + let suggestion = match name.name() { + "when" => "e.g. false, MyCustomFunction", + "skip_def" => "e.g. true, false", + "name" => "e.g. any_channel_name", + _ => "", + }; + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Missing value for watch argument {suggestion}"), + span.clone(), + )); + } + let value_pair = maybe_value_pair?; + + let value = match value_pair.as_rule() { + Rule::watch_argument_value => parse_watch_argument_value(value_pair, diagnostics)?, + _ => { + parsing_catch_all(value_pair, "watch decorator argument", diagnostics); + return None; + } + }; + + Some(WatchArgument { name, value, span }) +} + +fn parse_watch_argument_value( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { + assert_correct_parser!(token, Rule::watch_argument_value); + let inner = token.into_inner().next()?; + let span = diagnostics.span(inner.as_span()); + + match inner.as_rule() { + Rule::expr_block | Rule::expression => parse_assignment_expr(diagnostics, inner, span), + _ => { + parsing_catch_all(inner, "watch decorator argument value", diagnostics); + None + } + } +} + +pub fn parse_expr_block(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::expr_block); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + let mut stmts = Vec::new(); + let mut expr = None; + let _open_bracket = tokens.next()?; + + // Collect all items first so we can gather every header before we bind them + // to statements. We need two passes: the first pass collects and normalizes + // the headers (including establishing their relative levels), the second + // pass walks the statements in source order and attaches those normalized + // headers. If we tried to attach while parsing in a single pass, headers + // appearing inside comment blocks would be seen after their statements and + // could not participate in markdown hierarchy normalization. + let mut items: Vec> = Vec::new(); + for item in tokens { + items.push(item); + } + + // Track headers with their hierarchy + // NB(sam): I don't entirely understand why we need to wrap Headers in Arc<>, + // but here are the notes from codex: + // + // Most AST nodes are owned outright—each node sits in exactly one place in + // the tree—so ordinary struct fields work fine. Header annotations are the + // odd case: the parser needs to attach the same logical header instance to + // multiple spots (statements, trailing expressions, top‑level block etc.) + // while also normalizing them later. To avoid copying or moving those + // structs repeatedly, the parser promotes headers into shared references + // (Arc
). That lets the first pass create and normalize a header + // once, stash it in the lookup map, and then hand out clones of the pointer + // wherever the header appears, without duplication or life‑time juggling. + // Functionally, Arc is central here because headers get reused across many + // nodes, not because other AST structures require special thread‑safety + // treatment. + // + let mut all_headers_in_block: Vec> = Vec::new(); + + // First pass: collect all headers + for item in &items { + if item.as_rule() == Rule::comment_block { + let headers = headers_from_comment_block(item.clone(), diagnostics); + if !headers.is_empty() { + all_headers_in_block.extend(headers); + } + } + } + + // normalize_headers adjusts header levels so the shallowest header in the + // scope becomes an h1 + normalize_headers(&mut all_headers_in_block); + + // Lookup by span so we can reuse normalized headers later. + let mut header_lookup: HashMap<(usize, usize), std::sync::Arc
> = HashMap::new(); + for header in &all_headers_in_block { + header_lookup.insert((header.span.start, header.span.end), header.clone()); + } + + // Second pass: process statements and expressions with normalized headers + let mut current_headers: Vec> = Vec::new(); + let mut headers_since_last_stmt: Vec> = Vec::new(); + + for item in items { + match item.as_rule() { + Rule::stmt => { + let maybe_stmt = parse_statement(item, diagnostics); + if let Some(mut stmt) = maybe_stmt { + // Clear headers since last statement & get an iterator for the current ones. + // Better wrt mem::take() since it keeps Vec's allocation. + let header_drain = headers_since_last_stmt.drain(..); + bind_headers_to_statement(&mut stmt, header_drain); + stmts.push(stmt); + } + } + Rule::expr_body_stmt => { + let maybe_stmt = parse_expr_body_statement(item, diagnostics); + if let Some(mut stmt) = maybe_stmt { + // Clear headers since last statement & get an iterator for the current ones. + let header_drain = headers_since_last_stmt.drain(..); + bind_headers_to_statement(&mut stmt, header_drain); + stmts.push(stmt); + } + } + Rule::expression => { + let maybe_expr = parse_expression(item, diagnostics); + if let Some(parsed_expr) = maybe_expr { + expr = Some(parsed_expr); + continue; + } + } + Rule::BLOCK_CLOSE => { + // Commentend out because we can't have blocks without return + // expressions otherwise. Plus we need functions with no return + // types as well. + + // if expr.is_none() { + // diagnostics.push_error(DatamodelError::new_static( + // "Function must end in an expression.", + // span.clone(), + // )); + // } + break; + } + Rule::NEWLINE => { + continue; + } + Rule::comment_block => { + let headers = headers_from_comment_block(item, diagnostics); + if headers.is_empty() { + continue; + } + for header in headers { + attach_header_if_known( + &header, + &header_lookup, + &mut current_headers, + &mut headers_since_last_stmt, + ); + } + } + Rule::empty_lines => { + // Skip empty lines in function bodies + continue; + } + _ => { + diagnostics.push_error(DatamodelError::new_static( + "Internal Error: Parser only allows statements and expressions in function body.", + span.clone() + )); + } + } + } + + // Recursively decide if the trailing expression should be a statement or + // really is a trailing expression that produces a value. + let is_return_value = expr.as_ref().is_some_and(|expr| match expr { + // Base case. Expression that produce some kind of value. + Expression::BoolValue(..) + | Expression::StringValue(..) + | Expression::RawStringValue(..) + | Expression::NumericValue(..) + | Expression::JinjaExpressionValue(..) + | Expression::Identifier(..) + | Expression::App(..) + | Expression::MethodCall { .. } + | Expression::ArrayAccess(..) + | Expression::FieldAccess(..) + | Expression::Array(..) + | Expression::Map(..) + | Expression::ClassConstructor(..) + | Expression::BinaryOperation { .. } + | Expression::UnaryOperation { .. } + | Expression::Paren(..) => true, + + // If the trailing expression happens to be a block, check if the + // block itself has a trailing expression that produces a value. + Expression::ExprBlock(block, _) => block.expr.is_some(), + + // If trailing expression is an if statement, check if the statment + // itself has a trailing expression. + Expression::If(_, if_branch, else_branch, _) => match if_branch.as_ref() { + Expression::ExprBlock(block, _) => block.expr.is_some(), + _ => match else_branch.as_ref().map(Box::as_ref) { + Some(Expression::ExprBlock(block, _)) => block.expr.is_some(), + // This should not happen since branches are always blocks. + _ => true, + }, + }, + + // TODO: Is this possible? + Expression::Lambda(..) => todo!("exprs that evaluate to lambda"), + }); + + // If the block actually returns a value, keep it as trailing expression. + // Otherwise, promote the expression to a statement. + let trailing_expr = if is_return_value { + expr.map(Box::new) + } else { + if let Some(expr) = expr { + stmts.push(Stmt::Expression(ExprStmt { + expr: expr.clone(), + annotations: vec![], + span: expr.span().clone(), + })); + } + + None + }; + + Some(ExpressionBlock { + stmts, + expr: trailing_expr, + expr_headers: headers_since_last_stmt, + }) +} + +fn headers_from_comment_block( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Vec> { + if token.as_rule() != Rule::comment_block { + return Vec::new(); + } + + let mut headers = Vec::new(); + for current in token.into_inner() { + if current.as_rule() == Rule::comment { + if let Some(header) = parse_comment_header_pair(¤t, diagnostics) { + headers.push(std::sync::Arc::new(header)); + } + } + } + headers +} + +pub(crate) fn parse_comment_header_pair( + comment: &Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option
{ + let span = diagnostics.span(comment.as_span()); + let mut text = comment.as_str().trim_start(); + if !text.starts_with("//") { + return None; + } + text = &text[2..]; + let text = text.trim_start(); + if !text.starts_with('#') { + return None; + } + + let mut level = 0usize; + for ch in text.chars() { + if ch == '#' { + level += 1; + } else { + break; + } + } + if level == 0 { + return None; + } + + let rest = text[level..].trim().to_string(); + + Some(Header { + level: level as u8, + title: rest, + span, + }) +} + +fn attach_header_if_known( + header: &std::sync::Arc
, + lookup: &HashMap<(usize, usize), std::sync::Arc
>, + current_headers: &mut Vec>, + headers_since_last_stmt: &mut Vec>, +) { + let key = (header.span.start, header.span.end); + if let Some(normalized_header) = lookup.get(&key) { + filter_headers_by_hierarchy(current_headers, normalized_header); + current_headers.push(normalized_header.clone()); + headers_since_last_stmt.push(normalized_header.clone()); + } +} + +/// Filter headers based on hierarchy rules (markdown-style nesting) +fn filter_headers_by_hierarchy( + pending_headers: &mut Vec>, + new_header: &std::sync::Arc
, +) { + // Remove headers that are at the same level or deeper than the new header + // This implements the markdown hierarchy where: + // - A new header at level N closes all headers at level N or higher + // - Headers at level N+1, N+2, etc. nest under the header at level N + pending_headers.retain(|header| header.level < new_header.level); +} + +/// Normalize headers within a single block according to the normalization rules: +/// - All headers within a block scope should start from level 1 +/// - Maintain relative hierarchy between headers +fn normalize_headers(headers: &mut Vec>) { + if headers.is_empty() { + return; + } + + // Find the minimum level to normalize from + let min_level = headers.iter().map(|h| h.level).min().unwrap(); + + // Only normalize if headers don't already start from level 1 + if min_level > 1 { + // Create new normalized headers + let mut normalized_headers = Vec::new(); + + for header in headers.iter() { + // Normalize by adjusting all levels to start from 1 + let new_level = header.level - min_level + 1; + + // Create new header with normalized level + let normalized_header = std::sync::Arc::new(Header { + level: new_level, + title: header.title.clone(), + span: header.span.clone(), + }); + + normalized_headers.push(normalized_header); + } + + // Replace the original headers with normalized ones + *headers = normalized_headers; + } +} + +/// Bind pending headers to a statement based on scope rules +fn bind_headers_to_statement( + stmt: &mut Stmt, + pending_headers: impl IntoIterator>, +) { + match stmt { + Stmt::Let(let_stmt) => { + let_stmt.annotations.extend(pending_headers); + } + Stmt::ForLoop(for_stmt) => { + for_stmt.annotations.extend(pending_headers); + } + Stmt::Expression(es) => { + es.annotations.extend(pending_headers); + } + Stmt::Assign(_) => { + // Assignments do not carry annotations + } + Stmt::AssignOp(_) => { + // Assignment operations do not carry annotations + } + Stmt::CForLoop(_) => { + // C-for loops do not carry annotations (for now) + } + Stmt::WhileLoop(_) => { + // While loops do not carry annotations (for now) + } + Stmt::Semicolon(_) => { + // Semicolon expressions do not carry annotations + } + Stmt::Break(_) => { + // Break statements do not carry annotations + } + Stmt::Continue(_) => { + // Continue statements do not carry annotations + } + Stmt::Return(_) => { + // Return statements do not carry annotations (for now) + } + Stmt::Assert(_) => { + // Assert statements do not carry annotations (for now) + } + } +} + +pub(crate) fn parse_fn_args(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Vec { + assert_correct_parser!(token, Rule::fn_args); + + token + .into_inner() + .filter_map(|item| parse_expression(item, diagnostics)) + .collect() +} + +pub fn parse_fn_app(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::fn_app); + + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + let fn_name = parse_identifier(tokens.next()?, diagnostics); + + let args = parse_fn_args(tokens.next()?, diagnostics); + + Some(Expression::App(App { + name: fn_name, + type_args: vec![], + args, + span, + })) +} + +/// Parse function application with generic type arguments. +/// +/// Grammar rules for this one are a little bit more complicated than for +/// normal functions so can't reuse parse_fn_app easily. +pub fn parse_generic_fn_app(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::generic_fn_app); + + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + // Grab name from generic_fn_app_identifier rule. + let fn_name = parse_identifier(tokens.next()?.into_inner().next()?, diagnostics); + + // Move into generic_fn_app_args rule. + tokens = tokens.next()?.into_inner(); + + // Parse type argument. Only one for now. + let type_arg = parse_field_type_chain(tokens.next()?, diagnostics)?; + + // Parse arguments. + let args = parse_fn_args(tokens.next()?, diagnostics); + + Some(Expression::App(App { + name: fn_name, + type_args: vec![type_arg], + args, + span, + })) +} + +pub fn parse_lambda(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::lambda); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + let mut args = ArgumentsList { + arguments: Vec::new(), + }; + parse_arguments_list(tokens.next()?, &mut args, &None, diagnostics); + let maybe_body = parse_function_body(tokens.next()?, diagnostics); + maybe_body.map(|body| Expression::Lambda(args, Box::new(body), span)) +} + +pub fn parse_function_body( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option { + parse_expr_block(token, diagnostics) +} + +pub fn parse_if_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { + assert_correct_parser!(token, Rule::if_expression); + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + + let condition_rule = + check_parentheses_around_rule(&mut tokens, diagnostics, "if expression condition")?; + + let condition = parse_block_aware_tail_expression(condition_rule, diagnostics)?; + + let then_branch_rule = tokens.next()?; + let then_branch_span = diagnostics.span(then_branch_rule.as_span()); + let then_branch = parse_expr_block(then_branch_rule, diagnostics)?; + + let else_branch = tokens.next().and_then(|else_branch_expr| { + let else_branch_span = diagnostics.span(else_branch_expr.as_span()); + + let else_branch = match else_branch_expr.as_rule() { + Rule::expr_block => parse_expr_block(else_branch_expr, diagnostics) + .map(|e| Box::new(Expression::ExprBlock(e, else_branch_span))), + + Rule::if_expression => parse_if_expression(else_branch_expr, diagnostics).map(Box::new), + + _ => unreachable_rule!(else_branch_expr, Rule::if_expression), + }; + else_branch + }); + + Some(Expression::If( + Box::new(condition), + Box::new(Expression::ExprBlock(then_branch, then_branch_span)), + else_branch, + span, + )) +} diff --git a/engine/baml-lib/ast/src/parser/parse_expression.rs b/engine/baml-lib/ast/src/parser/parse_expression.rs index d0f32f2857..6b16978439 100644 --- a/engine/baml-lib/ast/src/parser/parse_expression.rs +++ b/engine/baml-lib/ast/src/parser/parse_expression.rs @@ -2,7 +2,7 @@ use baml_types::JinjaExpression; use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, unreachable_rule, Pair}, parse_expr::{ parse_expr_block, parse_expr_fn, parse_fn_app, parse_generic_fn_app, parse_if_expression, parse_lambda, @@ -11,10 +11,8 @@ use super::{ Rule, }; use crate::{ - assert_correct_parser, ast::*, parser::parse_expr::{consume_if_rule, consume_span_if_rule}, - unreachable_rule, }; pub(crate) fn parse_expression( @@ -23,7 +21,7 @@ pub(crate) fn parse_expression( ) -> Option { use pest::pratt_parser::{Assoc, Op, PrattParser}; - assert_correct_parser!(token, Rule::expression); + assert_correct_parser(&token, &[Rule::expression], diagnostics); // TODO: Initialize this shit once and pass it in (consider parallel parsing with .par_iter(), use some sync once cell or something). let pratt = PrattParser::new() @@ -69,7 +67,7 @@ pub(crate) fn parse_expression( let operator = match operator.as_rule() { Rule::NEG => UnaryOperator::Neg, Rule::NOT => UnaryOperator::Not, - _ => unreachable_rule!(operator, Rule::prefix_operator), + _ => unreachable!("Unexpected prefix operator: {:?}", operator.as_rule()), }; right.map(|right| Expression::UnaryOperation { @@ -126,10 +124,10 @@ pub(crate) fn parse_expression( } }, - _ => unreachable_rule!(inner, Rule::method_call), + _ => unreachable!("Unexpected method call rule: {:?}", inner.as_rule()), } } - _ => unreachable_rule!(operator, Rule::postfix_operator), + _ => unreachable!("Unexpected postfix operator: {:?}", operator.as_rule()), }) }) .map_infix(|left, operator, right| { @@ -153,7 +151,7 @@ pub(crate) fn parse_expression( Rule::OR => BinaryOperator::Or, Rule::AND => BinaryOperator::And, Rule::INSTANCE_OF => BinaryOperator::InstanceOf, - _ => unreachable_rule!(operator, Rule::infix_operator), + _ => unreachable!("Unexpected infix operator: {:?}", operator.as_rule()), }; Some(Expression::BinaryOperation { @@ -218,7 +216,10 @@ fn parse_primary_expression( None } - _ => unreachable_rule!(token, Rule::primary_expression), + _ => { + unreachable_rule(&token, "primary_expression", diagnostics); + None + } } } @@ -241,7 +242,7 @@ fn parse_array(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { ), ); } - _ => parsing_catch_all(current, "array"), + _ => parsing_catch_all(current, "array", diagnostics), } } @@ -249,7 +250,7 @@ fn parse_array(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { } fn parse_string_literal(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { - assert_correct_parser!(token, Rule::string_literal); + assert_correct_parser(&token, &[Rule::string_literal], diagnostics); let contents = token.clone().into_inner().next().unwrap(); let span = diagnostics.span(contents.as_span()); match contents.as_rule() { @@ -278,7 +279,10 @@ fn parse_string_literal(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expre } } } - _ => unreachable_rule!(contents, Rule::string_literal), + _ => { + unreachable_rule(&contents, "string_literal", diagnostics); + Expression::StringValue(String::new(), span) + } } } @@ -287,7 +291,7 @@ fn parse_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { pair: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Option<(Expression, Expression)> { - assert_correct_parser!(pair, Rule::expr_map_entry); + assert_correct_parser(&pair, &[Rule::expr_map_entry], diagnostics); let mut inner = pair.into_inner(); @@ -316,7 +320,7 @@ fn parse_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { pair: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Option<(Expression, Expression)> { - assert_correct_parser!(pair, Rule::ident_map_entry); + assert_correct_parser(&pair, &[Rule::ident_map_entry], diagnostics); let mut inner = pair.into_inner(); @@ -337,7 +341,10 @@ fn parse_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { match pair.as_rule() { Rule::expr_map_entry => parse_expr_map_entry(pair, diagnostics), Rule::ident_map_entry => parse_ident_map_entry(pair, diagnostics), - _ => unreachable_rule!(pair, Rule::map_expression), + _ => { + unreachable_rule(&pair, "map_expression", diagnostics); + None + } } } @@ -378,7 +385,7 @@ pub fn parse_config_expression( token: Pair<'_>, diagnostics: &mut internal_baml_diagnostics::Diagnostics, ) -> Option { - assert_correct_parser!(token, Rule::config_expression); + assert_correct_parser(&token, &[Rule::config_expression], diagnostics); parse_config_primary_expression(token.into_inner().next()?, diagnostics) } @@ -386,7 +393,7 @@ pub fn parse_config_primary_expression( token: Pair<'_>, diagnostics: &mut internal_baml_diagnostics::Diagnostics, ) -> Option { - assert_correct_parser!(token, Rule::config_primary_expression); + assert_correct_parser(&token, &[Rule::config_primary_expression], diagnostics); let span = diagnostics.span(token.as_span()); let token = token.into_inner().next()?; @@ -398,7 +405,10 @@ pub fn parse_config_primary_expression( Rule::jinja_expression => Some(parse_jinja_expression(token, diagnostics)), Rule::config_map_expression => Some(parse_config_map(token, diagnostics)), Rule::identifier => Some(Expression::Identifier(parse_identifier(token, diagnostics))), - _ => unreachable_rule!(token, Rule::config_primary_expression), + _ => { + unreachable_rule(&token, "config_primary_expression", diagnostics); + None + } } } @@ -421,7 +431,7 @@ fn parse_config_array(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Express ), ); } - _ => parsing_catch_all(current, "array"), + _ => parsing_catch_all(current, "array", diagnostics), } } @@ -440,7 +450,7 @@ fn parse_config_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expressio } } Rule::BLOCK_LEVEL_CATCH_ALL => {} - _ => parsing_catch_all(current, "config map key value"), + _ => parsing_catch_all(current, "config map key value", diagnostics), } } @@ -451,7 +461,7 @@ fn parse_config_map_entry( token: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Option<(Expression, Expression)> { - assert_correct_parser!(token, Rule::config_map_entry); + assert_correct_parser(&token, &[Rule::config_map_entry], diagnostics); let mut key = None; let mut value = None; @@ -482,7 +492,7 @@ fn parse_config_map_entry( return None; } Rule::BLOCK_LEVEL_CATCH_ALL => {} - _ => parsing_catch_all(current, "config dict entry"), + _ => parsing_catch_all(current, "config dict entry", diagnostics), } } @@ -502,7 +512,7 @@ fn parse_config_map_entry( } fn parse_config_map_key(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { - assert_correct_parser!(token, Rule::config_map_key); + assert_correct_parser(&token, &[Rule::config_map_key], diagnostics); let span = diagnostics.span(token.as_span()); if let Some(current) = token.into_inner().next() { @@ -512,14 +522,17 @@ fn parse_config_map_key(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expre current.into_inner().next().unwrap().as_str().to_string(), span, ), - _ => unreachable_rule!(current, Rule::config_map_key), + _ => { + unreachable_rule(¤t, "config_map_key", diagnostics); + Expression::Identifier(Identifier::Local(String::new(), span)) + } }; } unreachable!("Encountered impossible config map key during parsing") } pub(super) fn parse_raw_string(token: Pair<'_>, diagnostics: &mut Diagnostics) -> RawString { - assert_correct_parser!(token, Rule::raw_string_literal); + assert_correct_parser(&token, &[Rule::raw_string_literal], diagnostics); let mut language = None; let mut content = None; @@ -540,7 +553,7 @@ pub(super) fn parse_raw_string(token: Pair<'_>, diagnostics: &mut Diagnostics) - diagnostics.span(current.as_span()), )); } - _ => unreachable_rule!(current, Rule::raw_string_literal), + _ => unreachable_rule(¤t, "raw_string_literal", diagnostics), }; } match content { @@ -592,7 +605,7 @@ fn unescape_string(val: &str) -> String { /// processing engine, not to break a Jinja Expression into two lines, /// therefor the backing string should be contain "\\n". pub fn parse_jinja_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { - assert_correct_parser!(token, Rule::jinja_expression); + assert_correct_parser(&token, &[Rule::jinja_expression], diagnostics); let value = token .into_inner() .map(|token| match token.as_rule() { @@ -611,7 +624,13 @@ pub fn parse_jinja_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> diagnostics.span(token.as_span()), ) } - _ => unreachable_rule!(token, Rule::jinja_expression), + _ => { + unreachable_rule(&token, "jinja_expression", diagnostics); + Expression::JinjaExpressionValue( + JinjaExpression(String::new()), + diagnostics.span(token.as_span()), + ) + } }) .next(); @@ -623,7 +642,7 @@ pub fn parse_jinja_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> } pub fn parse_class_constructor(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { - assert_correct_parser!(token, Rule::class_constructor); + assert_correct_parser(&token, &[Rule::class_constructor], diagnostics); let span = diagnostics.span(token.as_span()); let mut tokens = token.into_inner(); @@ -648,7 +667,11 @@ pub fn parse_class_constructor(token: Pair<'_>, diagnostics: &mut Diagnostics) - continue; } - assert_correct_parser!(field_or_close_bracket, Rule::class_field_value_pair); + assert_correct_parser( + &field_or_close_bracket, + &[Rule::class_field_value_pair], + diagnostics, + ); let mut field_tokens = field_or_close_bracket.into_inner(); let identifier_or_spread = field_tokens.next().expect("Guaranteed by the grammar"); @@ -677,7 +700,7 @@ pub fn parse_class_constructor(token: Pair<'_>, diagnostics: &mut Diagnostics) - fields.push(ClassConstructorField::Named(field_name, expr)); } } - _ => unreachable_rule!(identifier_or_spread, Rule::class_field_value_pair), + _ => unreachable_rule(&identifier_or_spread, "class_field_value_pair", diagnostics), } let _maybe_comma = tokens.next(); } diff --git a/engine/baml-lib/ast/src/parser/parse_expression.rs.bak b/engine/baml-lib/ast/src/parser/parse_expression.rs.bak new file mode 100644 index 0000000000..acfc30c8ef --- /dev/null +++ b/engine/baml-lib/ast/src/parser/parse_expression.rs.bak @@ -0,0 +1,829 @@ +use baml_types::JinjaExpression; +use internal_baml_diagnostics::{DatamodelError, Diagnostics}; + +use super::{ + helpers::{assert_correct_parser, parsing_catch_all, unreachable_rule, Pair}, + parse_expr::{ + parse_expr_block, parse_expr_fn, parse_fn_app, parse_generic_fn_app, parse_if_expression, + parse_lambda, + }, + parse_identifier::parse_identifier, + Rule, +}; +use crate::{ + ast::*, + parser::parse_expr::{consume_if_rule, consume_span_if_rule}, +}; + +pub(crate) fn parse_expression( + token: Pair<'_>, + diagnostics: &mut internal_baml_diagnostics::Diagnostics, +) -> Option { + use pest::pratt_parser::{Assoc, Op, PrattParser}; + + assert_correct_parser!(token, Rule::expression); + + // TODO: Initialize this shit once and pass it in (consider parallel parsing with .par_iter(), use some sync once cell or something). + let pratt = PrattParser::new() + .op(Op::infix(Rule::OR, Assoc::Left)) + .op(Op::infix(Rule::AND, Assoc::Left)) + .op(Op::infix(Rule::EQ, Assoc::Left) + | Op::infix(Rule::NEQ, Assoc::Left) + | Op::infix(Rule::LT, Assoc::Left) + | Op::infix(Rule::LTEQ, Assoc::Left) + | Op::infix(Rule::GT, Assoc::Left) + | Op::infix(Rule::GTEQ, Assoc::Left) + | Op::infix(Rule::INSTANCE_OF, Assoc::Left)) + .op(Op::infix(Rule::BIT_OR, Assoc::Left)) + .op(Op::infix(Rule::BIT_XOR, Assoc::Left)) + .op(Op::infix(Rule::BIT_AND, Assoc::Left)) + .op(Op::infix(Rule::BIT_SHL, Assoc::Left) | Op::infix(Rule::BIT_SHR, Assoc::Left)) + .op(Op::infix(Rule::ADD, Assoc::Left) | Op::infix(Rule::SUB, Assoc::Left)) + .op(Op::infix(Rule::MUL, Assoc::Left) + | Op::infix(Rule::DIV, Assoc::Left) + | Op::infix(Rule::MOD, Assoc::Left)) + .op(Op::prefix(Rule::NOT)) + .op(Op::prefix(Rule::NEG)) + .op(Op::postfix(Rule::array_accessor)) + .op(Op::postfix(Rule::method_call)) + .op(Op::postfix(Rule::field_accessor)); + + let span = diagnostics.span(token.as_span()); + + let diagnostics_ptr: *mut internal_baml_diagnostics::Diagnostics = diagnostics; + + let mut parser = pratt + .map_primary(|primary| { + // Ah yes, Rust superiority. + #[allow(unsafe_code)] + let diagnostics = unsafe { &mut *diagnostics_ptr }; + + match primary.as_rule() { + Rule::expression => parse_expression(primary, diagnostics), + _ => parse_primary_expression(primary.into_inner().next()?, diagnostics), + } + }) + .map_prefix(|operator, right| { + let operator = match operator.as_rule() { + Rule::NEG => UnaryOperator::Neg, + Rule::NOT => UnaryOperator::Not, + _ => unreachable_rule!(operator, Rule::prefix_operator), + }; + + right.map(|right| Expression::UnaryOperation { + operator, + expr: Box::new(right), + span: span.clone(), + }) + }) + .map_postfix(|left, operator| { + let left = left?; + + Some(match operator.as_rule() { + Rule::array_accessor => { + let index = parse_expression(operator.into_inner().next()?, diagnostics)?; + + Expression::ArrayAccess(Box::new(left), Box::new(index), span.clone()) + } + + Rule::field_accessor => { + let field = parse_identifier(operator.into_inner().next()?, diagnostics); + + Expression::FieldAccess(Box::new(left), field, span.clone()) + } + + Rule::method_call => { + let inner = operator.into_inner().next()?; + + match inner.as_rule() { + Rule::fn_app => match parse_fn_app(inner, diagnostics)? { + Expression::App(fn_call) => Expression::MethodCall { + receiver: Box::new(left), + method: fn_call.name, + args: fn_call.args, + type_args: fn_call.type_args, + span: span.clone(), + }, + + _ => { + unreachable!("expected function call when parsing method call") + } + }, + + Rule::generic_fn_app => match parse_generic_fn_app(inner, diagnostics)? { + Expression::App(fn_call) => Expression::MethodCall { + receiver: Box::new(left), + method: fn_call.name, + args: fn_call.args, + type_args: fn_call.type_args, + span: span.clone(), + }, + + _ => { + unreachable!("expected function call when parsing method call") + } + }, + + _ => unreachable_rule!(inner, Rule::method_call), + } + } + _ => unreachable_rule!(operator, Rule::postfix_operator), + }) + }) + .map_infix(|left, operator, right| { + let operator = match operator.as_rule() { + Rule::EQ => BinaryOperator::Eq, + Rule::NEQ => BinaryOperator::Neq, + Rule::LT => BinaryOperator::Lt, + Rule::LTEQ => BinaryOperator::LtEq, + Rule::GT => BinaryOperator::Gt, + Rule::GTEQ => BinaryOperator::GtEq, + Rule::ADD => BinaryOperator::Add, + Rule::SUB => BinaryOperator::Sub, + Rule::MUL => BinaryOperator::Mul, + Rule::DIV => BinaryOperator::Div, + Rule::MOD => BinaryOperator::Mod, + Rule::BIT_AND => BinaryOperator::BitAnd, + Rule::BIT_OR => BinaryOperator::BitOr, + Rule::BIT_XOR => BinaryOperator::BitXor, + Rule::BIT_SHL => BinaryOperator::Shl, + Rule::BIT_SHR => BinaryOperator::Shr, + Rule::OR => BinaryOperator::Or, + Rule::AND => BinaryOperator::And, + Rule::INSTANCE_OF => BinaryOperator::InstanceOf, + _ => unreachable_rule!(operator, Rule::infix_operator), + }; + + Some(Expression::BinaryOperation { + left: Box::new(left?), + operator, + right: Box::new(right?), + span: span.clone(), + }) + }); + + parser.parse(token.into_inner()) +} + +fn parse_primary_expression( + token: Pair<'_>, + diagnostics: &mut internal_baml_diagnostics::Diagnostics, +) -> Option { + let span = diagnostics.span(token.as_span()); + match token.as_rule() { + Rule::numeric_literal => Some(Expression::NumericValue(token.as_str().into(), span)), + Rule::string_literal => Some(parse_string_literal(token, diagnostics)), + Rule::raw_string_literal => Some(Expression::RawStringValue(parse_raw_string( + token, + diagnostics, + ))), + Rule::quoted_string_literal => { + let contents = token.into_inner().next().unwrap(); + Some(Expression::StringValue( + unescape_string(contents.as_str()), + span, + )) + } + Rule::map_expression => Some(parse_map(token, diagnostics)), + Rule::array_expression => Some(parse_array(token, diagnostics)), + Rule::jinja_expression => Some(parse_jinja_expression(token, diagnostics)), + Rule::identifier => match token.as_str() { + "true" => Some(Expression::BoolValue(true, span)), + "false" => Some(Expression::BoolValue(false, span)), + _ => Some(Expression::Identifier(parse_identifier(token, diagnostics))), + }, + Rule::class_constructor => Some(parse_class_constructor(token, diagnostics)), + Rule::fn_app => parse_fn_app(token, diagnostics), + Rule::generic_fn_app => parse_generic_fn_app(token, diagnostics), + Rule::lambda => parse_lambda(token, diagnostics), + Rule::expr_block => { + parse_expr_block(token, diagnostics).map(|block| Expression::ExprBlock(block, span)) + } + Rule::if_expression => parse_if_expression(token, diagnostics), + + // Nested expr in parens. + Rule::expression => { + parse_expression(token, diagnostics).map(|expr| Expression::Paren(Box::new(expr), span)) + } + + Rule::BLOCK_LEVEL_CATCH_ALL => { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "This is not a valid expression!", + span, + ), + ); + None + } + + _ => unreachable_rule!(token, Rule::primary_expression), + } +} + +fn parse_array(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + let mut elements: Vec = vec![]; + let span = token.as_span(); + + for current in token.into_inner() { + match current.as_rule() { + Rule::expression => { + if let Some(expr) = parse_expression(current, diagnostics) { + elements.push(expr); + } + } + Rule::ARRAY_CATCH_ALL => { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "Invalid array syntax detected.", + diagnostics.span(current.as_span()), + ), + ); + } + _ => parsing_catch_all(current, "array", diagnostics), + } + } + + Expression::Array(elements, diagnostics.span(span)) +} + +fn parse_string_literal(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + assert_correct_parser!(token, Rule::string_literal); + let contents = token.clone().into_inner().next().unwrap(); + let span = diagnostics.span(contents.as_span()); + match contents.as_rule() { + Rule::raw_string_literal => { + Expression::RawStringValue(parse_raw_string(contents, diagnostics)) + } + Rule::quoted_string_literal => { + let contents = contents.into_inner().next().unwrap(); + Expression::StringValue(unescape_string(contents.as_str()), span) + } + Rule::unquoted_string_literal => { + let raw_content = contents.as_str(); + // If the content starts or ends with a space, trim it + let content = raw_content.trim().to_string(); + + if content.contains(' ') { + Expression::StringValue(content, span) + } else if content.eq("true") || content.eq("false") { + Expression::BoolValue(content.eq("true"), span) + } else { + match Identifier::from((content.as_str(), span.clone())) { + Identifier::Invalid(..) | Identifier::String(..) => { + Expression::StringValue(content, span) + } + identifier => Expression::Identifier(identifier), + } + } + } + _ => unreachable_rule!(contents, Rule::string_literal), + } +} + +fn parse_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + fn parse_expr_map_entry( + pair: Pair<'_>, + diagnostics: &mut Diagnostics, + ) -> Option<(Expression, Expression)> { + assert_correct_parser!(pair, Rule::expr_map_entry); + + let mut inner = pair.into_inner(); + + let key_rule = inner.next()?; + let colon = consume_if_rule(&mut inner, Rule::COLON); + let value_rule = inner.next()?; + + let key = parse_expression(key_rule, diagnostics)?; + let value = parse_expression(value_rule, diagnostics)?; + + if colon.is_none() { + diagnostics.push_error(DatamodelError::new_validation_error( + "Missing colon between key expression & value expression", + Span { + file: key.span().file.clone(), + start: key.span().end, + end: value.span().start, + }, + )); + } + + Some((key, value)) + } + + fn parse_ident_map_entry( + pair: Pair<'_>, + diagnostics: &mut Diagnostics, + ) -> Option<(Expression, Expression)> { + assert_correct_parser!(pair, Rule::ident_map_entry); + + let mut inner = pair.into_inner(); + + let ident = parse_identifier(inner.next()?, diagnostics); + + let value = parse_expression(inner.next()?, diagnostics)?; + + Some(( + Expression::StringValue(ident.to_string(), ident.span().clone()), + value, + )) + } + + fn parse_map_entry( + pair: Pair<'_>, + diagnostics: &mut Diagnostics, + ) -> Option<(Expression, Expression)> { + match pair.as_rule() { + Rule::expr_map_entry => parse_expr_map_entry(pair, diagnostics), + Rule::ident_map_entry => parse_ident_map_entry(pair, diagnostics), + _ => unreachable_rule!(pair, Rule::map_expression), + } + } + + let span = token.as_span(); + + let mut inner = token + .into_inner() + .filter(|pair| !matches!(pair.as_rule(), Rule::NEWLINE)); + + // Option<(rule, span of inference)> + // We'll be reporting + + let entries = if let Some(first) = inner.next() { + let first_rule = first.as_rule(); + + let first_entry = parse_map_entry(first, diagnostics).into_iter(); + + let rest_of_entries = inner.filter_map(|pair| { + + if first_rule != pair.as_rule() { + diagnostics.push_error(DatamodelError::new_validation_error("Inconsistent use of key-value pair syntax. Consider using python-style if any of the keys is an identifier to avoid confusion", diagnostics.span(pair.as_span()))); + } + + parse_map_entry(pair, diagnostics) + + + }); + + first_entry.chain(rest_of_entries).collect() + } else { + Vec::new() + }; + + Expression::Map(entries, diagnostics.span(span)) +} + +pub fn parse_config_expression( + token: Pair<'_>, + diagnostics: &mut internal_baml_diagnostics::Diagnostics, +) -> Option { + assert_correct_parser!(token, Rule::config_expression); + parse_config_primary_expression(token.into_inner().next()?, diagnostics) +} + +pub fn parse_config_primary_expression( + token: Pair<'_>, + diagnostics: &mut internal_baml_diagnostics::Diagnostics, +) -> Option { + assert_correct_parser!(token, Rule::config_primary_expression); + let span = diagnostics.span(token.as_span()); + + let token = token.into_inner().next()?; + + match token.as_rule() { + Rule::numeric_literal => Some(Expression::NumericValue(token.as_str().into(), span)), + Rule::string_literal => Some(parse_string_literal(token, diagnostics)), + Rule::config_array_expression => Some(parse_config_array(token, diagnostics)), + Rule::jinja_expression => Some(parse_jinja_expression(token, diagnostics)), + Rule::config_map_expression => Some(parse_config_map(token, diagnostics)), + Rule::identifier => Some(Expression::Identifier(parse_identifier(token, diagnostics))), + _ => unreachable_rule!(token, Rule::config_primary_expression), + } +} + +fn parse_config_array(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + let mut elements: Vec = vec![]; + let span = token.as_span(); + + for current in token.into_inner() { + match current.as_rule() { + Rule::config_expression => { + if let Some(expr) = parse_config_expression(current, diagnostics) { + elements.push(expr); + } + } + Rule::ARRAY_CATCH_ALL => { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "Invalid array syntax detected.", + diagnostics.span(current.as_span()), + ), + ); + } + _ => parsing_catch_all(current, "array", diagnostics), + } + } + + Expression::Array(elements, diagnostics.span(span)) +} + +fn parse_config_map(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + let mut entries: Vec<(Expression, Expression)> = vec![]; + let span = token.as_span(); + + for current in token.into_inner() { + match current.as_rule() { + Rule::config_map_entry => { + if let Some(f) = parse_config_map_entry(current, diagnostics) { + entries.push(f) + } + } + Rule::BLOCK_LEVEL_CATCH_ALL => {} + _ => parsing_catch_all(current, "config map key value", diagnostics), + } + } + + Expression::Map(entries, diagnostics.span(span)) +} + +fn parse_config_map_entry( + token: Pair<'_>, + diagnostics: &mut Diagnostics, +) -> Option<(Expression, Expression)> { + assert_correct_parser!(token, Rule::config_map_entry); + + let mut key = None; + let mut value = None; + let token_span = token.as_span(); // Store the span before moving token + + for current in token.into_inner() { + match current.as_rule() { + Rule::config_map_key => key = Some(parse_config_map_key(current, diagnostics)), + Rule::config_expression => value = parse_config_expression(current, diagnostics), + Rule::COLON => { + if key.is_none() { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "This map entry is missing a valid key or has an incorrect syntax.", + diagnostics.span(token_span), // Use the stored span here + ), + ); + return None; + } + } + Rule::ENTRY_CATCH_ALL => { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "This map entry is missing a valid value or has an incorrect syntax.", + diagnostics.span(token_span), // Use the stored span here + ), + ); + return None; + } + Rule::BLOCK_LEVEL_CATCH_ALL => {} + _ => parsing_catch_all(current, "config dict entry", diagnostics), + } + } + + match (key, value) { + (Some(key), Some(value)) => Some((key, value)), + (Some(_), None) => { + diagnostics.push_error( + internal_baml_diagnostics::DatamodelError::new_validation_error( + "This map entry is missing a valid value or has an incorrect syntax.", + diagnostics.span(token_span), // Use the stored span here + ), + ); + None + } + _ => None, + } +} + +fn parse_config_map_key(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + assert_correct_parser!(token, Rule::config_map_key); + + let span = diagnostics.span(token.as_span()); + if let Some(current) = token.into_inner().next() { + return match current.as_rule() { + Rule::identifier => Expression::Identifier(parse_identifier(current, diagnostics)), + Rule::quoted_string_literal => Expression::StringValue( + current.into_inner().next().unwrap().as_str().to_string(), + span, + ), + _ => unreachable_rule!(current, Rule::config_map_key), + }; + } + unreachable!("Encountered impossible config map key during parsing") +} + +pub(super) fn parse_raw_string(token: Pair<'_>, diagnostics: &mut Diagnostics) -> RawString { + assert_correct_parser!(token, Rule::raw_string_literal); + + let mut language = None; + let mut content = None; + + for current in token.into_inner() { + match current.as_rule() { + Rule::single_word => { + let contents = current.as_str().to_string(); + language = Some((contents, diagnostics.span(current.as_span()))); + } + Rule::raw_string_literal_content_1 + | Rule::raw_string_literal_content_2 + | Rule::raw_string_literal_content_3 + | Rule::raw_string_literal_content_4 + | Rule::raw_string_literal_content_5 => { + content = Some(( + current.as_str().to_string(), + diagnostics.span(current.as_span()), + )); + } + _ => unreachable_rule!(current, Rule::raw_string_literal), + }; + } + match content { + Some((content, span)) => RawString::new(content, span, language), + _ => unreachable!("Encountered impossible raw string during parsing"), + } +} + +// NOTE(sam): this doesn't handle unicode escape sequences e.g. \u1234 +// also this has panicks in it (see the hex logic) +fn unescape_string(val: &str) -> String { + let mut result = String::with_capacity(val.len()); + let mut chars = val.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some('n') => result.push('\n'), + Some('r') => result.push('\r'), + Some('t') => result.push('\t'), + Some('0') => result.push('\0'), + Some('\'') => result.push('\''), + Some('\"') => result.push('\"'), + Some('\\') => result.push('\\'), + Some('x') => { + let mut hex = String::new(); + hex.push(chars.next().unwrap()); + hex.push(chars.next().unwrap()); + result.push(u8::from_str_radix(&hex, 16).unwrap() as char); + } + Some(c) => { + result.push('\\'); + result.push(c); + } + None => result.push('\\'), + } + } else { + result.push(c); + } + } + + result +} + +/// Parse a `JinjaExpression` from raw source. Escape backslashes, +/// because we want the user's backslash intent to be preserved in +/// the string backing the `JinjaExpression`. In other words, control +/// sequences like `\n` are intended to be forwarded to the Jinja +/// processing engine, not to break a Jinja Expression into two lines, +/// therefor the backing string should be contain "\\n". +pub fn parse_jinja_expression(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + assert_correct_parser!(token, Rule::jinja_expression); + let value = token + .into_inner() + .map(|token| match token.as_rule() { + Rule::jinja_body => { + let mut inner_text = String::new(); + for c in token.as_str().chars() { + match c { + // When encountering a single backslash, produce two backslashes. + '\\' => inner_text.push_str("\\\\"), + // Otherwise, just copy the character. + _ => inner_text.push(c), + } + } + Expression::JinjaExpressionValue( + JinjaExpression(inner_text), + diagnostics.span(token.as_span()), + ) + } + _ => unreachable_rule!(token, Rule::jinja_expression), + }) + .next(); + + if let Some(value) = value { + value + } else { + unreachable!("Encountered impossible jinja expression during parsing") + } +} + +pub fn parse_class_constructor(token: Pair<'_>, diagnostics: &mut Diagnostics) -> Expression { + assert_correct_parser!(token, Rule::class_constructor); + + let span = diagnostics.span(token.as_span()); + let mut tokens = token.into_inner(); + let class_name = parse_identifier( + tokens.next().expect("Guaranteed by the grammar"), + diagnostics, + ); + let mut fields = Vec::new(); + while let Some(field_or_close_bracket) = tokens.next() { + if field_or_close_bracket.as_str() == "}" { + break; + } + if field_or_close_bracket.as_str() == "," { + continue; + } + if field_or_close_bracket.as_rule() == Rule::NEWLINE { + continue; + } + + assert_correct_parser!(field_or_close_bracket, Rule::class_field_value_pair); + + let mut field_tokens = field_or_close_bracket.into_inner(); + let identifier_or_spread = field_tokens.next().expect("Guaranteed by the grammar"); + match identifier_or_spread.as_rule() { + Rule::struct_spread => { + let mut struct_spread_tokens = identifier_or_spread.into_inner(); + let maybe_expr = parse_expression( + struct_spread_tokens + .next() + .expect("Guaranteed by the grammar"), + diagnostics, + ); + if let Some(expr) = maybe_expr { + fields.push(ClassConstructorField::Spread(expr)); + } + } + Rule::identifier => { + let field_name = parse_identifier(identifier_or_spread, diagnostics); + + let _colon = field_tokens.next(); + let maybe_expr = parse_expression( + field_tokens.next().expect("Guaranteed by the grammar"), + diagnostics, + ); + if let Some(expr) = maybe_expr { + fields.push(ClassConstructorField::Named(field_name, expr)); + } + } + _ => unreachable_rule!(identifier_or_spread, Rule::class_field_value_pair), + } + let _maybe_comma = tokens.next(); + } + let class_constructor = ClassConstructor { class_name, fields }; + + Expression::ClassConstructor(class_constructor, span) +} + +#[cfg(test)] +mod tests { + use internal_baml_diagnostics::{Diagnostics, SourceFile}; + use pest::{consumes_to, parses_to, Parser}; + + use super::{ + super::{parse_expr::parse_expr_block, BAMLParser, Rule}, + *, + }; + + #[test] + fn test_parse_jinja_expression() { + let input = "{{ 1 + 1 }}"; + let root_path = "test_file.baml"; + let source = SourceFile::new_static(root_path.into(), input); + let mut diagnostics = Diagnostics::new(root_path.into()); + diagnostics.set_source(&source); + + let pair = BAMLParser::parse(Rule::jinja_expression, input) + .unwrap() + .next() + .unwrap(); + let expr = parse_jinja_expression(pair, &mut diagnostics); + match expr { + Expression::JinjaExpressionValue(JinjaExpression(s), _) => assert_eq!(s, "1 + 1"), + _ => panic!("Expected JinjaExpression, got {expr:?}"), + } + } + + #[test] + fn test_comment_header_parsing() { + println!("\n=== Testing Comment Header Parsing ==="); + + let input = r#"{ + //# Level 1 Header + let x = "hello"; + + //## Level 2 Header + let y = "world"; + + //########### Level 11 Header + + //### Level 3 Headers + x + y + }"#; + + let root_path = "test_file.baml"; + let source = SourceFile::new_static(root_path.into(), input); + let mut diagnostics = Diagnostics::new(root_path.into()); + diagnostics.set_source(&source); + + println!("Parsing expression block with comment headers..."); + + let pair_result = BAMLParser::parse(Rule::expr_block, input); + match pair_result { + Ok(mut pairs) => { + let pair = pairs.next().unwrap(); + let expr = parse_expr_block(pair, &mut diagnostics); + match expr { + Some(expr_block) => { + println!("✓ Successfully parsed expression block: {expr_block:?}") + } + None => println!("✗ Failed to parse expression block"), + } + } + Err(e) => println!("✗ Parse error: {e:?}"), + } + + println!("Diagnostics:"); + for error in diagnostics.errors() { + println!(" Error: {error:?}"); + } + for warning in diagnostics.warnings() { + println!(" Warning: {warning:?}"); + } + } + + #[test] + fn test_complex_header_hierarchy() { + println!("\n=== Testing Complex Header Hierarchy ==="); + + let input = r#"//# Loop Processing +fn ForLoopWithHeaders() -> int { + let items = [1, 2, 3, 4, 5]; + let result = 0; + + //## Main Loop + for (item in items) { + //### Item Processing + let processed = item * 2; + + //#### Accumulation + result = result + processed; + } + + //## Final Result + result +}"#; + + let root_path = "test_file.baml"; + let source = SourceFile::new_static(root_path.into(), input); + let mut diagnostics = Diagnostics::new(root_path.into()); + diagnostics.set_source(&source); + + println!("Parsing function with complex header hierarchy..."); + + let pair_result = BAMLParser::parse(Rule::schema, input); + match pair_result { + Ok(mut pairs) => { + let schema_pair = pairs.next().unwrap(); + println!("✓ Successfully parsed schema"); + + // Look for expr_fn within the schema + for item in schema_pair.into_inner() { + match item.as_rule() { + Rule::expr_fn => { + let expr_fn = parse_expr_fn(item, &mut diagnostics); + match expr_fn { + Some(expr_fn) => { + println!( + "✓ Found and parsed function: {}", + expr_fn.name.name() + ); + } + None => println!("✗ Failed to parse function"), + } + } + Rule::comment_block => { + println!("✓ Found top-level comment block"); + } + _ => { + println!("Found other item: {:?}", item.as_rule()); + } + } + } + } + Err(e) => { + println!("✗ Parse error: {e:?}"); + return; + } + } + + println!("Diagnostics errors: {}", diagnostics.errors().len()); + for error in diagnostics.errors() { + println!(" Error: {error:?}"); + } + } +} diff --git a/engine/baml-lib/ast/src/parser/parse_field.rs b/engine/baml-lib/ast/src/parser/parse_field.rs index c8d86b61c9..e7623fb13c 100644 --- a/engine/baml-lib/ast/src/parser/parse_field.rs +++ b/engine/baml-lib/ast/src/parser/parse_field.rs @@ -22,7 +22,8 @@ pub(crate) fn parse_value_expr( let mut name: Option = None; let mut attributes: Vec = Vec::new(); let mut field_type = None; - let mut comment: Option = block_comment.and_then(parse_comment_block); + let mut comment: Option = + block_comment.and_then(|c| parse_comment_block(c, diagnostics)); for current in pair.into_inner() { match current.as_rule() { @@ -31,7 +32,7 @@ pub(crate) fn parse_value_expr( attributes.push(parse_attribute(current, false, diagnostics)); } Rule::trailing_comment => { - comment = match (comment, parse_trailing_comment(current)) { + comment = match (comment, parse_trailing_comment(current, diagnostics)) { (c, None) | (None, c) => c, (Some(existing), Some(new)) => Some(Comment { text: [existing.text, new.text].join("\n"), @@ -43,7 +44,7 @@ pub(crate) fn parse_value_expr( field_type = Some(parse_config_expression(current, diagnostics)) } - _ => parsing_catch_all(current, "field"), + _ => parsing_catch_all(current, "field", diagnostics), } } @@ -97,7 +98,8 @@ pub(crate) fn parse_type_expr( let mut name: Option = None; let mut field_attributes = Vec::::new(); let mut field_type = None; - let mut comment: Option = block_comment.and_then(parse_comment_block); + let mut comment: Option = + block_comment.and_then(|c| parse_comment_block(c, diagnostics)); for current in pair.into_inner() { match current.as_rule() { @@ -105,7 +107,7 @@ pub(crate) fn parse_type_expr( name = Some(parse_identifier(current, diagnostics)); } Rule::trailing_comment => { - comment = merge_comments(comment, parse_trailing_comment(current)); + comment = merge_comments(comment, parse_trailing_comment(current, diagnostics)); } Rule::field_type_chain => { field_type = parse_field_type_chain(current, diagnostics); @@ -114,7 +116,7 @@ pub(crate) fn parse_type_expr( let attribute = parse_attribute(current, false, diagnostics); field_attributes.push(attribute); } - _ => parsing_catch_all(current, "field"), + _ => parsing_catch_all(current, "field", diagnostics), } } @@ -204,7 +206,7 @@ pub(crate) fn parse_field_type_with_attr( } Rule::trailing_comment => {} _ => { - parsing_catch_all(current, "field_type_with_attr!"); + parsing_catch_all(current, "field_type_with_attr!", diagnostics); } } } diff --git a/engine/baml-lib/ast/src/parser/parse_identifier.rs b/engine/baml-lib/ast/src/parser/parse_identifier.rs index aed9c51cf3..a090db843f 100644 --- a/engine/baml-lib/ast/src/parser/parse_identifier.rs +++ b/engine/baml-lib/ast/src/parser/parse_identifier.rs @@ -1,36 +1,37 @@ use internal_baml_diagnostics::{DatamodelError, Diagnostics}; -use super::helpers::Pair; +use super::helpers::{assert_correct_parser, unreachable_rule, Pair}; use crate::{ - assert_correct_parser, ast::{Identifier, RefIdentifier, WithName}, parser::Rule, - unreachable_rule, }; pub fn parse_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { - assert_correct_parser!(pair, Rule::identifier); + assert_correct_parser(&pair, &[Rule::identifier], diagnostics); if let Some(inner) = pair.into_inner().next() { return match inner.as_rule() { Rule::path_identifier => parse_path_identifier(inner, diagnostics), Rule::namespaced_identifier => parse_namespaced_identifier(inner, diagnostics), Rule::single_word => parse_single_word(inner, diagnostics), - _ => unreachable_rule!(inner, Rule::identifier), + _ => { + unreachable_rule(&inner, "identifier", diagnostics); + Identifier::Local(String::new(), diagnostics.span(inner.as_span())) + } }; } unreachable!("Encountered impossible identifier during parsing.") } fn parse_single_word(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { - assert_correct_parser!(pair, Rule::single_word); + assert_correct_parser(&pair, &[Rule::single_word], diagnostics); let span = diagnostics.span(pair.as_span()); Identifier::from((pair.as_str(), span)) } pub fn parse_path_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { - assert_correct_parser!(pair, Rule::path_identifier); + assert_correct_parser(&pair, &[Rule::path_identifier], diagnostics); let span = diagnostics.span(pair.as_span()); let raw_str = pair.as_str(); @@ -38,7 +39,7 @@ pub fn parse_path_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> I for inner in pair.into_inner() { match inner.as_rule() { Rule::single_word => vec.push(inner.as_str()), - _ => unreachable_rule!(inner, Rule::path_identifier), + _ => unreachable_rule(&inner, "path_identifier", diagnostics), } } @@ -69,7 +70,7 @@ pub fn parse_path_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> I /// TODO: `Identifier` should eventually store the namespace components /// individually. fn parse_namespaced_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { - assert_correct_parser!(pair, Rule::namespaced_identifier); + assert_correct_parser(&pair, &[Rule::namespaced_identifier], diagnostics); let raw_str = pair.as_str(); let span = diagnostics.span(pair.as_span()); @@ -77,7 +78,7 @@ fn parse_namespaced_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> for inner in pair.into_inner() { match inner.as_rule() { Rule::single_word => name_parts.push(inner.as_str()), - _ => unreachable_rule!(inner, Rule::namespaced_identifier), + _ => unreachable_rule(&inner, "namespaced_identifier", diagnostics), } } diff --git a/engine/baml-lib/ast/src/parser/parse_identifier.rs.bak b/engine/baml-lib/ast/src/parser/parse_identifier.rs.bak new file mode 100644 index 0000000000..aed9c51cf3 --- /dev/null +++ b/engine/baml-lib/ast/src/parser/parse_identifier.rs.bak @@ -0,0 +1,92 @@ +use internal_baml_diagnostics::{DatamodelError, Diagnostics}; + +use super::helpers::Pair; +use crate::{ + assert_correct_parser, + ast::{Identifier, RefIdentifier, WithName}, + parser::Rule, + unreachable_rule, +}; + +pub fn parse_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { + assert_correct_parser!(pair, Rule::identifier); + + if let Some(inner) = pair.into_inner().next() { + return match inner.as_rule() { + Rule::path_identifier => parse_path_identifier(inner, diagnostics), + Rule::namespaced_identifier => parse_namespaced_identifier(inner, diagnostics), + Rule::single_word => parse_single_word(inner, diagnostics), + _ => unreachable_rule!(inner, Rule::identifier), + }; + } + unreachable!("Encountered impossible identifier during parsing.") +} + +fn parse_single_word(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { + assert_correct_parser!(pair, Rule::single_word); + let span = diagnostics.span(pair.as_span()); + + Identifier::from((pair.as_str(), span)) +} + +pub fn parse_path_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { + assert_correct_parser!(pair, Rule::path_identifier); + + let span = diagnostics.span(pair.as_span()); + let raw_str = pair.as_str(); + let mut vec = vec![]; + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::single_word => vec.push(inner.as_str()), + _ => unreachable_rule!(inner, Rule::path_identifier), + } + } + + // TODO: THIS IS SUSPECT + assert!( + vec.len() > 1, + "Path identifier must have at least 2 elements. Path({}) Raw({})", + vec.join("."), + raw_str + ); + + if vec[0] == "env" { + let env_name = vec[1..].join("."); + return Identifier::ENV(env_name, span); + } + + Identifier::Ref( + RefIdentifier { + path: vec[..vec.len() - 1].iter().map(|s| s.to_string()).collect(), + name: vec[vec.len() - 1].to_string(), + full_name: vec.join("."), + }, + span, + ) +} + +/// Parse an identifier of the form `word::word::word` directly into a that string. +/// TODO: `Identifier` should eventually store the namespace components +/// individually. +fn parse_namespaced_identifier(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Identifier { + assert_correct_parser!(pair, Rule::namespaced_identifier); + + let raw_str = pair.as_str(); + let span = diagnostics.span(pair.as_span()); + let mut name_parts = Vec::new(); + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::single_word => name_parts.push(inner.as_str()), + _ => unreachable_rule!(inner, Rule::namespaced_identifier), + } + } + + assert!( + name_parts.len() > 1, + "Namespaced identifier must have at least 2 elements. Parts({}) Raw({})", + name_parts.join("::"), + raw_str + ); + + Identifier::Local(name_parts.join("::"), span) +} diff --git a/engine/baml-lib/ast/src/parser/parse_named_args_list.rs b/engine/baml-lib/ast/src/parser/parse_named_args_list.rs index 03157db6c6..b49c72f69f 100644 --- a/engine/baml-lib/ast/src/parser/parse_named_args_list.rs +++ b/engine/baml-lib/ast/src/parser/parse_named_args_list.rs @@ -2,12 +2,11 @@ use internal_baml_diagnostics::DatamodelError; // Add this line use internal_baml_diagnostics::{Diagnostics, Span}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, Pair}, parse_field::parse_field_type_chain, parse_identifier::parse_identifier, }; use crate::{ - assert_correct_parser, ast::{BlockArg, BlockArgs, FieldArity, FieldType, Identifier, WithName, WithSpan}, parser::Rule, }; @@ -29,7 +28,7 @@ pub(crate) fn parse_named_argument_list( } if named_arg.as_rule() == Rule::named_argument || named_arg.as_rule() == Rule::openParen { // TODO: THIS IS SUSPECT - assert_correct_parser!(named_arg, named_arg.as_rule()); + assert_correct_parser(&named_arg, &[named_arg.as_rule()], diagnostics); } // TODO: THIS IS SUSPECT // assert_correct_parser!(named_arg, Rule::named_argument); @@ -60,7 +59,7 @@ pub(crate) fn parse_named_argument_list( Err(e) => diagnostics.push_error(e), } } - _ => parsing_catch_all(arg, "named_argument_list"), + _ => parsing_catch_all(arg, "named_argument_list", diagnostics), } } diff --git a/engine/baml-lib/ast/src/parser/parse_template_string.rs b/engine/baml-lib/ast/src/parser/parse_template_string.rs index f36c076286..6de567f944 100644 --- a/engine/baml-lib/ast/src/parser/parse_template_string.rs +++ b/engine/baml-lib/ast/src/parser/parse_template_string.rs @@ -35,7 +35,7 @@ pub(crate) fn parse_template_string( diagnostics, ))) } - _ => parsing_catch_all(current, "function"), + _ => parsing_catch_all(current, "function", diagnostics), } } @@ -48,7 +48,8 @@ pub(crate) fn parse_template_string( input, value: prompt, attributes, - documentation: doc_comment.and_then(parse_comment_block), + documentation: doc_comment + .and_then(|c| parse_comment_block(c, diagnostics)), span: diagnostics.span(pair_span), }); } diff --git a/engine/baml-lib/ast/src/parser/parse_type_builder_block.rs b/engine/baml-lib/ast/src/parser/parse_type_builder_block.rs index 1f655d0428..6a89216d8f 100644 --- a/engine/baml-lib/ast/src/parser/parse_type_builder_block.rs +++ b/engine/baml-lib/ast/src/parser/parse_type_builder_block.rs @@ -1,11 +1,10 @@ use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, Pair}, Rule, }; use crate::{ - assert_correct_parser, ast::*, parser::{ parse_assignment::parse_assignment, @@ -17,7 +16,7 @@ pub fn parse_type_builder_block( pair: Pair<'_>, diagnostics: &mut Diagnostics, ) -> Result { - assert_correct_parser!(pair, Rule::type_builder_block); + assert_correct_parser(&pair, &[Rule::type_builder_block], diagnostics); let span = diagnostics.span(pair.as_span()); let mut entries = Vec::new(); @@ -38,7 +37,7 @@ pub fn parse_type_builder_block( // Last token, closing bracket. Rule::BLOCK_CLOSE => {} - _ => parsing_catch_all(current, "type_builder_block"), + _ => parsing_catch_all(current, "type_builder_block", diagnostics), } } @@ -50,7 +49,7 @@ pub fn parse_type_builder_contents( entries: &mut Vec, diagnostics: &mut Diagnostics, ) { - assert_correct_parser!(pair, Rule::type_builder_contents); + assert_correct_parser(&pair, &[Rule::type_builder_contents], diagnostics); let mut pending_block_comment = None; @@ -96,7 +95,9 @@ pub fn parse_type_builder_contents( } } - _ => parsing_catch_all(nested, "dynamic_type_expression_block"), + _ => { + parsing_catch_all(nested, "dynamic_type_expression_block", diagnostics) + } } } } @@ -124,7 +125,7 @@ pub fn parse_type_builder_contents( )) } - _ => parsing_catch_all(current, "type_builder_contents"), + _ => parsing_catch_all(current, "type_builder_contents", diagnostics), } } } diff --git a/engine/baml-lib/ast/src/parser/parse_type_expression_block.rs b/engine/baml-lib/ast/src/parser/parse_type_expression_block.rs index a8b81d27a2..8f683a8a4c 100644 --- a/engine/baml-lib/ast/src/parser/parse_type_expression_block.rs +++ b/engine/baml-lib/ast/src/parser/parse_type_expression_block.rs @@ -1,7 +1,7 @@ use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{ - helpers::{parsing_catch_all, Pair}, + helpers::{assert_correct_parser, parsing_catch_all, Pair}, parse_attribute::parse_attribute, parse_comments::*, parse_identifier::parse_identifier, @@ -9,17 +9,16 @@ use super::{ Rule, }; use crate::{ - assert_correct_parser, ast::{TypeExpressionBlock, *}, parser::{parse_expr::parse_expr_fn, parse_field::parse_type_expr}, -}; // Add this line to import DatamodelParser +}; pub(crate) fn parse_type_expression_block( pair: Pair<'_>, doc_comment: Option>, diagnostics: &mut Diagnostics, ) -> TypeExpressionBlock { - assert_correct_parser!(pair, Rule::type_expression_block); + assert_correct_parser(&pair, &[Rule::type_expression_block], diagnostics); let pair_span = pair.as_span(); let mut name: Option = None; @@ -132,12 +131,12 @@ pub(crate) fn parse_type_expression_block( diagnostics.span(item.as_span()), )) } - _ => parsing_catch_all(item, "type_expression"), + _ => parsing_catch_all(item, "type_expression", diagnostics), } } } - _ => parsing_catch_all(current, "type_expression"), + _ => parsing_catch_all(current, "type_expression", diagnostics), } } @@ -161,7 +160,7 @@ pub(crate) fn parse_type_expression_block( methods, input, attributes, - documentation: doc_comment.and_then(parse_comment_block), + documentation: doc_comment.and_then(|c| parse_comment_block(c, diagnostics)), span: diagnostics.span(pair_span), sub_type: sub_type.0, type_span: diagnostics.span(sub_type.1), diff --git a/engine/baml-lib/ast/src/parser/parse_types.rs b/engine/baml-lib/ast/src/parser/parse_types.rs index 2fd82cf355..f7e363bece 100644 --- a/engine/baml-lib/ast/src/parser/parse_types.rs +++ b/engine/baml-lib/ast/src/parser/parse_types.rs @@ -5,17 +5,20 @@ use internal_baml_diagnostics::{DatamodelError, Diagnostics}; use super::{helpers::Pair, parse_attribute::parse_attribute, Rule}; use crate::{ - assert_correct_parser, ast::*, parser::{ - helpers::parsing_catch_all, parse_field::parse_field_type_with_attr, + helpers::{assert_correct_parser, parsing_catch_all, unreachable_rule}, + parse_field::parse_field_type_with_attr, parse_identifier::parse_identifier, }, - unreachable_rule, }; pub fn parse_field_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::field_type, Rule::openParen, Rule::closeParen); + assert_correct_parser( + &pair, + &[Rule::field_type, Rule::openParen, Rule::closeParen], + diagnostics, + ); let mut arity = FieldArity::Required; let mut ftype = None; @@ -37,7 +40,7 @@ pub fn parse_field_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option } Rule::optional_token => arity = FieldArity::Optional, _ => { - parsing_catch_all(current, "field_type"); + parsing_catch_all(current, "field_type", diagnostics); } } } @@ -57,7 +60,7 @@ pub fn parse_field_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option } fn parse_union(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::union); + assert_correct_parser(&pair, &[Rule::union], diagnostics); let span = diagnostics.span(pair.as_span()); let mut types = Vec::new(); @@ -75,7 +78,7 @@ fn parse_union(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option {} - _ => unreachable_rule!(current, Rule::union), + _ => unreachable_rule(¤t, "union", diagnostics), } } @@ -107,7 +110,7 @@ fn parse_base_type_with_attr(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> O let att = parse_attribute(current, false, diagnostics); attributes.push(att); } - _ => unreachable_rule!(current, Rule::base_type_with_attr), + _ => unreachable_rule(¤t, "base_type_with_attr", diagnostics), } } @@ -121,11 +124,14 @@ fn parse_base_type_with_attr(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> O } fn parse_base_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!( - pair, - Rule::base_type, - Rule::non_union, - Rule::base_type_without_array + assert_correct_parser( + &pair, + &[ + Rule::base_type, + Rule::non_union, + Rule::base_type_without_array, + ], + diagnostics, ); if let Some(current) = pair.into_inner().next() { @@ -192,7 +198,10 @@ fn parse_base_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option parse_tuple(current, diagnostics), Rule::parenthesized_type => parse_parenthesized_type(current, diagnostics), Rule::literal_type => parse_literal_type(current, diagnostics), - _ => unreachable_rule!(current, Rule::base_type), + _ => { + unreachable_rule(¤t, "base_type", diagnostics); + None + } }; } @@ -200,7 +209,7 @@ fn parse_base_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::parenthesized_type); + assert_correct_parser(&pair, &[Rule::parenthesized_type], diagnostics); for current in pair.into_inner() { match current.as_rule() { @@ -208,7 +217,7 @@ fn parse_parenthesized_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Op Rule::field_type_with_attr => { return parse_field_type_with_attr(current, true, diagnostics); } - _ => unreachable_rule!(current, Rule::parenthesized_type), + _ => unreachable_rule(¤t, "parenthesized_type", diagnostics), } } @@ -216,7 +225,7 @@ fn parse_parenthesized_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Op } fn parse_literal_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::literal_type); + assert_correct_parser(&pair, &[Rule::literal_type], diagnostics); let span = diagnostics.span(pair.as_span()); @@ -247,7 +256,10 @@ fn parse_literal_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option unreachable_rule!(literal_type, Rule::literal_type), + _ => { + unreachable_rule(&literal_type, "literal_type", diagnostics); + LiteralValue::String(String::new()) + } }; Some(FieldType::Literal( @@ -277,7 +289,7 @@ fn parse_literal_type(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::array_notation); + assert_correct_parser(&pair, &[Rule::array_notation], diagnostics); let mut dims = 0_u32; let mut field = None; @@ -295,7 +307,9 @@ fn parse_array(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option arity = FieldArity::Optional, - _ => unreachable_rule!(current, Rule::map), + _ => { + unreachable_rule(¤t, "map", diagnostics); + } } } @@ -334,7 +348,7 @@ fn parse_array(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option`, `map?`. fn parse_map(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::map); + assert_correct_parser(&pair, &[Rule::map], diagnostics); let mut fields = Vec::new(); // Track whether this map is optional (e.g., map?) @@ -353,7 +367,9 @@ fn parse_map(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option // Handle optional marker (?) for maps like map? // This makes the entire map optional, not its values Rule::optional_token => arity = FieldArity::Optional, - _ => unreachable_rule!(current, Rule::map), + _ => { + unreachable_rule(¤t, "map", diagnostics); + } } } @@ -371,7 +387,7 @@ fn parse_map(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option } fn parse_group(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::group); + assert_correct_parser(&pair, &[Rule::group], diagnostics); let mut attributes = Vec::new(); let mut field_type = None; @@ -385,7 +401,9 @@ fn parse_group(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option unreachable_rule!(current, Rule::group), + _ => { + unreachable_rule(¤t, "group", diagnostics); + } } } @@ -397,7 +415,7 @@ fn parse_group(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option, diagnostics: &mut Diagnostics) -> Option { - assert_correct_parser!(pair, Rule::tuple); + assert_correct_parser(&pair, &[Rule::tuple], diagnostics); let span = diagnostics.span(pair.as_span()); @@ -417,7 +435,9 @@ fn parse_tuple(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Option unreachable_rule!(current, Rule::tuple), + _ => { + unreachable_rule(¤t, "tuple", diagnostics); + } } } diff --git a/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs b/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs index fd2e164264..e541634a01 100644 --- a/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs +++ b/engine/baml-lib/ast/src/parser/parse_value_expression_block.rs @@ -139,11 +139,11 @@ pub(crate) fn parse_value_expression_block( diagnostics.span(item.as_span()), )) } - _ => parsing_catch_all(item, "model"), + _ => parsing_catch_all(item, "model", diagnostics), } } } - _ => parsing_catch_all(current, "function"), + _ => parsing_catch_all(current, "function", diagnostics), } } @@ -176,7 +176,7 @@ pub(crate) fn parse_value_expression_block( output, attributes, fields, - documentation: doc_comment.and_then(parse_comment_block), + documentation: doc_comment.and_then(|c| parse_comment_block(c, diagnostics)), span: diagnostics.span(pair_span), type_builder, block_type: sub_type.unwrap_or(ValueExprBlockType::Function), diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit.baml b/integ-tests/baml_src/test-files/workflows/workflow_watch.baml similarity index 100% rename from integ-tests/baml_src/test-files/workflows/workflow_emit.baml rename to integ-tests/baml_src/test-files/workflows/workflow_watch.baml diff --git a/integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml b/integ-tests/baml_src/test-files/workflows/workflow_watch_simple.baml similarity index 100% rename from integ-tests/baml_src/test-files/workflows/workflow_emit_simple.baml rename to integ-tests/baml_src/test-files/workflows/workflow_watch_simple.baml diff --git a/integ-tests/go/baml_client/baml_source_map.go b/integ-tests/go/baml_client/baml_source_map.go index 56b3424498..a29299cdab 100644 --- a/integ-tests/go/baml_client/baml_source_map.go +++ b/integ-tests/go/baml_client/baml_source_map.go @@ -133,8 +133,8 @@ var file_map = map[string]string{ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } func getBamlFiles() map[string]string { diff --git a/integ-tests/python-v1/baml_client/inlinedbaml.py b/integ-tests/python-v1/baml_client/inlinedbaml.py index 092268b47b..f4490f7447 100644 --- a/integ-tests/python-v1/baml_client/inlinedbaml.py +++ b/integ-tests/python-v1/baml_client/inlinedbaml.py @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/python/baml_client/inlinedbaml.py b/integ-tests/python/baml_client/inlinedbaml.py index 092268b47b..f4490f7447 100644 --- a/integ-tests/python/baml_client/inlinedbaml.py +++ b/integ-tests/python/baml_client/inlinedbaml.py @@ -130,8 +130,8 @@ "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } def get_baml_files(): diff --git a/integ-tests/react/baml_client/inlinedbaml.ts b/integ-tests/react/baml_client/inlinedbaml.ts index e5ff2d3262..f4ee1a642d 100644 --- a/integ-tests/react/baml_client/inlinedbaml.ts +++ b/integ-tests/react/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts index e5ff2d3262..f4ee1a642d 100644 --- a/integ-tests/typescript-esm/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript-esm/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap; diff --git a/integ-tests/typescript/baml_client/inlinedbaml.ts b/integ-tests/typescript/baml_client/inlinedbaml.ts index e5ff2d3262..f4ee1a642d 100644 --- a/integ-tests/typescript/baml_client/inlinedbaml.ts +++ b/integ-tests/typescript/baml_client/inlinedbaml.ts @@ -138,8 +138,8 @@ const fileMap = { "test-files/tools/todo-llm.baml": "class AddTodoItem {\n type \"add_todo_item\" @stream.not_null\n item string\n time string\n description string @description(\"20 word description of the item\")\n @@stream.done\n}\n\nclass TodoMessageToUser {\n type \"todo_message_to_user\" @stream.not_null\n message string @description(\"A message to the user, about 50 words long\")\n}\n\ntype TodoTool = AddTodoItem | TodoMessageToUser\n\nfunction ChooseTodoTools(query: string) -> TodoTool[] {\n client GPT4\n prompt #\"\n Choose tools to satisfy the user query.\n For example, if they ask for \"5 todo items for learning chess\",\n return a list of 5 \"add_todo_item\" objects and single \"todo_message_to_user\"\n object. All requests should end with a \"todo_message_to_user\" object.\n\n {{ ctx.output_format }}\n \"#\n}\n\ntest ChooseTodoTools {\n functions [ChooseTodoTools]\n args {\n query \"5 todo items for learning chess\"\n }\n}\n", "test-files/vm/expr_funcs.baml": "// --------------------------------\n// Pure Expression Functions.\n// --------------------------------\n\nfunction ReturnOne() -> int {\n 1\n}\n\nfunction ReturnNumber(n: int) -> int {\n n\n}\n\nfunction CallReturnOne() -> int {\n ReturnOne()\n}\n\nfunction ChainedCalls() -> int {\n ReturnNumber(CallReturnOne())\n}\n\nfunction StoreFnCallInLocalVar(n: int) -> int {\n let result = ReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElse(b: bool) -> int {\n if (b) { 1 } else { 0 }\n}\n\nfunction ReturnElseIfExpr(a: bool, b: bool) -> int {\n if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n }\n}\n\nfunction AssignElseIfExpr(a: bool, b: bool) -> int {\n let result = if (a) {\n 1\n } else if (b) {\n 2\n } else {\n 3\n };\n\n result\n}\n\nfunction NormalElseIfStmt(a: bool, b: bool) -> int {\n let v = 0;\n\n if (a) {\n let one = 1;\n StoreFnCallInLocalVar(one);\n } else if (b) {\n let two = 2;\n StoreFnCallInLocalVar(two);\n } else {\n let three = 3;\n StoreFnCallInLocalVar(three);\n }\n\n v\n}\n\nfunction IterativeFibonacci(n: int) -> int {\n let a = 0;\n let b = 1;\n\n if (n == 0) {\n b\n } else {\n let i = 1;\n while (i <= n) {\n let c = a + b;\n a = b;\n b = c;\n i += 1;\n }\n a\n }\n}\n\nfunction SumArray(arr: int[]) -> int {\n let sum = 0;\n for (let a in arr) {\n sum += a;\n }\n sum\n}\n\nfunction SumFromTo(x: int, y: int) -> int {\n let s = 0;\n\n for (let i = x; i <= y; i += 1) {\n s += i;\n }\n\n s\n}\n\nfunction ReturnCategory(category: Category) -> Category {\n category\n}\n\nfunction ReturnImageFromUrl(url: string) -> image {\n image.from_url(url)\n}\n\nfunction HomeEnvVarIsEmpty() -> bool {\n let home = env.HOME;\n home == \"\"\n}\n\n// --------------------------------\n// Expression Functions with LLM calls.\n// --------------------------------\n\nfunction CallLlmDescribeImage(img: image) -> string {\n DescribeImage(img)\n}\n\nfunction LlmReturnNumber(n: int) -> int {\n client GPT35LegacyProvider // GPT 3.5 Turbo\n prompt #\"\n Return the number {{ n }} without any additional text.\n \"#\n}\n\nfunction ReturnNumberCallingLlm(n: int) -> int {\n LlmReturnNumber(n)\n}\n\nfunction StoreLlmCallInLocalVar(n: int) -> int {\n let result = LlmReturnNumber(n);\n\n result\n}\n\nfunction BoolToIntWithIfElseCallingLlm(b: bool) -> int {\n if (b) { LlmReturnNumber(1) } else { LlmReturnNumber(0) }\n}\n\n/// Other builtins\n\nclass DummyJsonTodo {\n id int\n todo string\n completed bool\n userId int\n}\n\nfunction ExecFetchAs(url: string) -> DummyJsonTodo {\n let todo = baml.fetch_as(url);\n\n todo\n}\n\ntest Fib5() {\n functions [IterativeFibonacci]\n args { n 5 }\n @@assert( {{ this == 5 }} )\n}\n", "test-files/workflows/workflow_basic.baml": "function LLMEcho(input: string) -> string {\n client GPT35\n prompt #\"Echo exactly the input: {{input}}\"#\n}\n\nfunction EchoWorkflow() -> string {\n let input = \"Hello, world!\";\n LLMEcho(input)\n}\n\ntest EchoWorkflow {\n functions [EchoWorkflow]\n args {}\n}\n", - "test-files/workflows/workflow_emit.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", - "test-files/workflows/workflow_emit_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch.baml": "function WorkflowWatch() -> int {\n watch let x: int = 10;\n watch let y: bool = true;\n watch let once: string = \"Hello\";\n watch let twice: string[] = [\"Takedown\", \"\"];\n watch let story = LLMEcho(#\"\n Hello world this is a test of a longish string.\n I want it to be long enough to generate a few chunks.\n \"#);\n\n y = false;\n x = WorkflowWatchChild();\n x = 101;\n x\n}\n\nfunction WorkflowWatchChild() -> int {\n watch let x: string = \"Hello\";\n 100\n}\n\nfunction AnotherTakedown(xs: string[]) -> int {\n xs[1] = \"Takedown\";\n xs = [\"Takedown\", \"Takedown\"];\n 0\n}\n\n// Filter function that uses an LLM to check if a word equals \"banana\"\n// This tests that filter functions can call LLMs\nfunction IsTargetWord(word: string) -> bool {\n let result = CheckWordEquality(word, \"banana\");\n result\n}\n\nfunction IsTargetWord2(word: string) -> bool {\n word == \"banana\"\n}\n\n// LLM function that checks if two words are equal\n// We use an LLM for this trivial task just to test LLM calling in filter functions\nfunction CheckWordEquality(word: string, target: string) -> bool {\n client GPT4Turbo\n prompt #\"\n Does the word \"{{ word }}\" equal \"{{ target }}\"?\n\n Respond with only true or false.\n \"#\n}\n\n// Test with LLM-based filter function\nfunction WorkflowWatchWithFilter() -> int {\n let words: string[] = [\"apple\", \"banana\", \"cherry\", \"banana\", \"date\",\n \"elderberry\", \"banana\", \"fig\", \"grape\", \"honeydew\"];\n\n // This variable will only notify watchers when the word is \"banana\" (as determined by LLM)\n watch let this_word: string = \"\";\n this_word.$watch.options( baml.WatchOptions{ when: IsTargetWord })\n\n let i: int = 0;\n for (let word in words) {\n this_word = word;\n i += 1;\n }\n\n // Should have notified 3 times (for the 3 \"banana\" occurrences at indices 1, 3, 6)\n i\n}\n\ntest TestWorkflowWatch() {\n functions [WorkflowWatch]\n args {}\n}\n\ntest TestWorkflowWatchWithFilter() {\n functions [WorkflowWatchWithFilter]\n args {}\n}\n", + "test-files/workflows/workflow_watch_simple.baml": "// Simple filter function that only notifies when value is not empty\nfunction NotEmpty(value: string) -> bool {\n value != \"\"\n}\n\n// Simple test without loops\nfunction SimpleWatchWithFilter() -> int {\n watch let word: string = \"\";\n word.$watch.options(baml.WatchOptions{ when: NotEmpty });\n\n word = \"hello\"; // Should notify (not empty)\n word = \"world\"; // Should notify (not empty)\n word = \"\"; // Should NOT notify (empty)\n word = \"test\"; // Should notify (not empty)\n\n word.$watch.options(baml.WatchOptions{ channel: \"new_name\" });\n word = \"with_new_name\"; // Should notify (not empty)\n word.$watch.notify();\n\n 42\n}\n\ntest TestSimpleWatchWithFilter() {\n functions [SimpleWatchWithFilter]\n args {}\n}\n", } export const getBamlFiles = () => { return fileMap;