diff --git a/engine/baml-lib/baml-core/src/lib.rs b/engine/baml-lib/baml-core/src/lib.rs index c9b5a0b93..65a53c6cb 100644 --- a/engine/baml-lib/baml-core/src/lib.rs +++ b/engine/baml-lib/baml-core/src/lib.rs @@ -234,6 +234,8 @@ pub fn validate_type_builder_entries( dyn_type.span.to_owned(), ); + // TODO: Not necessary, the parser also does this now that we've + // change "dynamic ClassName" to "dynamic class ClassName". dyn_type.is_dynamic_type_def = true; // Resolve dynamic definition. It either appends to a @@ -245,7 +247,18 @@ pub fn validate_type_builder_entries( diagnostics.push_error(DatamodelError::new_validation_error( &format!( "Type '{}' does not contain the `@@dynamic` attribute so it cannot be modified in a type builder block", - cls.name() + cls.name() + ), + dyn_type.span.to_owned(), + )); + continue; + } + + if matches!(dyn_type.sub_type, ast::SubType::Enum) { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!( + "Type '{}' is a class, but the dynamic block is defined as 'dynamic enum'", + cls.name() ), dyn_type.span.to_owned(), )); @@ -266,6 +279,17 @@ pub fn validate_type_builder_entries( continue; } + if matches!(dyn_type.sub_type, ast::SubType::Class) { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!( + "Type '{}' is an enum, but the dynamic block is defined as 'dynamic class'", + enm.name() + ), + dyn_type.span.to_owned(), + )); + continue; + } + ast::Top::Enum(dyn_type) }, TypeWalker::TypeAlias(_) => { diff --git a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types.baml b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types.baml index 2df0d9280..e480870af 100644 --- a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types.baml +++ b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types.baml @@ -11,6 +11,12 @@ class Education { year int } +enum Job { + SoftwareEngineer + DataScientist + @@dynamic +} + // This function returns the dynamic class defined above. function ExtractResume(from_text: string) -> Resume { client "openai/gpt-4o-mini" @@ -28,11 +34,22 @@ test ReturnDynamicClassTest { start_date string end_date string } + + enum Level { + Junior + Mid + Senior + } // This `dynamic` block is used to inject new properties into the // `@@dynamic` part of the Resume class. - dynamic Resume { + dynamic class Resume { experience Experience[] + level Level + } + + dynamic enum Job { + ProductManager } } args { diff --git a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_external_cycle_errors.baml b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_external_cycle_errors.baml index d0bf67621..a4e24ee0f 100644 --- a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_external_cycle_errors.baml +++ b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_external_cycle_errors.baml @@ -22,7 +22,7 @@ test AttemptToIntroduceInfiniteCycle { p A } - dynamic DynamicClass { + dynamic class DynamicClass { cycle A } } @@ -34,7 +34,7 @@ test AttemptToIntroduceInfiniteCycle { test AttemptToMakeClassInfinitelyRecursive { functions [TypeBuilderFn] type_builder { - dynamic DynamicClass { + dynamic class DynamicClass { cycle DynamicClass } } diff --git a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_internal_cycle_errors.baml b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_internal_cycle_errors.baml index f3fb4ed7a..758808cbe 100644 --- a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_internal_cycle_errors.baml +++ b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_internal_cycle_errors.baml @@ -12,7 +12,7 @@ class DynamicClass { test AttemptToMakeClassInfinitelyRecursive { functions [TypeBuilderFn] type_builder { - dynamic DynamicClass { + dynamic class DynamicClass { cycle DynamicClass } } @@ -25,7 +25,7 @@ test AttemptToMakeClassInfinitelyRecursive { // --> tests/dynamic_types_internal_cycle_errors.baml:15 // | // 14 | type_builder { -// 15 | dynamic DynamicClass { +// 15 | dynamic class DynamicClass { // 16 | cycle DynamicClass // 17 | } // | diff --git a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_parser_errors.baml b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_parser_errors.baml index 2f1f4e03f..f433b18a0 100644 --- a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_parser_errors.baml +++ b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_parser_errors.baml @@ -5,7 +5,7 @@ function TypeBuilderFn(from_text: string) -> Resume { class Foo { foo string } - dynamic Bar { + dynamic class Bar { bar int } } @@ -17,7 +17,7 @@ test MultipleTypeBuilderBlocks { class Foo { foo string } - dynamic Bar { + dynamic class Bar { bar int } } @@ -25,7 +25,7 @@ test MultipleTypeBuilderBlocks { class A { a string } - dynamic B { + dynamic class B { b int } } @@ -34,12 +34,24 @@ test MultipleTypeBuilderBlocks { } } +test IncompleteDynamicDef { + functions [TypeBuilderFn] + type_builder { + dynamic Bar { + bar int + } + } + args { + from_text "Test" + } +} + test IncompleteSyntax { functions [TypeBuilderFn] type_builder { type - dynamic Bar { + dynamic class Bar { bar int } } @@ -56,7 +68,7 @@ test IncompleteSyntax { // 5 | class Foo { // 6 | foo string // 7 | } -// 8 | dynamic Bar { +// 8 | dynamic class Bar { // 9 | bar int // 10 | } // 11 | } @@ -69,15 +81,21 @@ test IncompleteSyntax { // 25 | class A { // 26 | a string // 27 | } -// 28 | dynamic B { +// 28 | dynamic class B { // 29 | b int // 30 | } // 31 | } // | -// error: Error validating: Syntax error in type builder block +// error: Error validating: Incomplete 'dynamic' type definition. Use 'dynamic class' or 'dynamic enum' to add properties to types that contain the `@@dynamic` attribute. // --> tests/dynamic_types_parser_errors.baml:40 // | // 39 | type_builder { -// 40 | type -// 41 | +// 40 | dynamic Bar { +// | +// error: Error validating: Syntax error in type builder block +// --> tests/dynamic_types_parser_errors.baml:52 +// | +// 51 | type_builder { +// 52 | type +// 53 | // | diff --git a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_validation_errors.baml b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_validation_errors.baml index e0b136448..b571d6d5d 100644 --- a/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_validation_errors.baml +++ b/engine/baml-lib/baml/tests/validation_files/tests/dynamic_types_validation_errors.baml @@ -11,7 +11,7 @@ class NonDynamic { test AttemptToModifyNonDynamicClass { functions [TypeBuilderFn] type_builder { - dynamic NonDynamic { + dynamic class NonDynamic { c string } } @@ -25,7 +25,7 @@ type SomeAlias = NonDynamic test AttemptToModifyTypeAlias { functions [TypeBuilderFn] type_builder { - dynamic SomeAlias { + dynamic class SomeAlias { c string } } @@ -51,7 +51,7 @@ test AttemptToAddDynamicAttrInDyanmicDef { A @@dynamic } - dynamic DynamicClass { + dynamic class DynamicClass { c string @@dynamic } @@ -64,10 +64,10 @@ test AttemptToAddDynamicAttrInDyanmicDef { test AttemptToModifySameDynamicMultipleTimes { functions [TypeBuilderFn] type_builder { - dynamic DynamicClass { + dynamic class DynamicClass { c string } - dynamic DynamicClass { + dynamic class DynamicClass { d string } } @@ -83,7 +83,7 @@ test NameAlreadyExists { a string b string } - dynamic DynamicClass { + dynamic class DynamicClass { non_dynamic NonDynamic } } @@ -92,11 +92,32 @@ test NameAlreadyExists { } } +enum DynamicEnum { + A + B + @@dynamic +} + +test TypeMismatch { + functions [TypeBuilderFn] + type_builder { + dynamic class DynamicEnum { + C + } + dynamic enum DynamicClass { + c string + } + } + args { + from_text "Test" + } +} + // error: Error validating: Type 'NonDynamic' does not contain the `@@dynamic` attribute so it cannot be modified in a type builder block // --> tests/dynamic_types_validation_errors.baml:14 // | // 13 | type_builder { -// 14 | dynamic NonDynamic { +// 14 | dynamic class NonDynamic { // 15 | c string // 16 | } // | @@ -104,7 +125,7 @@ test NameAlreadyExists { // --> tests/dynamic_types_validation_errors.baml:28 // | // 27 | type_builder { -// 28 | dynamic SomeAlias { +// 28 | dynamic class SomeAlias { // 29 | c string // 30 | } // | @@ -130,7 +151,7 @@ test NameAlreadyExists { // --> tests/dynamic_types_validation_errors.baml:54 // | // 53 | } -// 54 | dynamic DynamicClass { +// 54 | dynamic class DynamicClass { // 55 | c string // 56 | @@dynamic // 57 | } @@ -139,7 +160,7 @@ test NameAlreadyExists { // --> tests/dynamic_types_validation_errors.baml:70 // | // 69 | } -// 70 | dynamic DynamicClass { +// 70 | dynamic class DynamicClass { // 71 | d string // 72 | } // | @@ -149,3 +170,19 @@ test NameAlreadyExists { // 81 | type_builder { // 82 | class NonDynamic { // | +// error: Error validating: Type 'DynamicEnum' is an enum, but the dynamic block is defined as 'dynamic class' +// --> tests/dynamic_types_validation_errors.baml:104 +// | +// 103 | type_builder { +// 104 | dynamic class DynamicEnum { +// 105 | C +// 106 | } +// | +// error: Error validating: Type 'DynamicClass' is a class, but the dynamic block is defined as 'dynamic enum' +// --> tests/dynamic_types_validation_errors.baml:107 +// | +// 106 | } +// 107 | dynamic enum DynamicClass { +// 108 | c string +// 109 | } +// | diff --git a/engine/baml-lib/schema-ast/src/ast/type_expression_block.rs b/engine/baml-lib/schema-ast/src/ast/type_expression_block.rs index 42716b0ce..8c2f01b57 100644 --- a/engine/baml-lib/schema-ast/src/ast/type_expression_block.rs +++ b/engine/baml-lib/schema-ast/src/ast/type_expression_block.rs @@ -27,7 +27,7 @@ impl std::ops::Index for TypeExpressionBlock { pub enum SubType { Enum, Class, - Dynamic, + Dynamic(Box), Other(String), } diff --git a/engine/baml-lib/schema-ast/src/parser/datamodel.pest b/engine/baml-lib/schema-ast/src/parser/datamodel.pest index 2161aa25c..2845d5dcf 100644 --- a/engine/baml-lib/schema-ast/src/parser/datamodel.pest +++ b/engine/baml-lib/schema-ast/src/parser/datamodel.pest @@ -6,6 +6,10 @@ schema = { // Unified Block for Class and Enum // ###################################### type_expression_block = { identifier ~ identifier ~ named_argument_list? ~ BLOCK_OPEN ~ type_expression_contents ~ BLOCK_CLOSE } + +// Dynamic declarations start with the dynamic keyword followed by a normal type expression. +dynamic_type_expression_block = { identifier ~ type_expression_block } + type_expression_contents = { (type_expression | block_attribute | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* } @@ -34,7 +38,7 @@ value_expression = { identifier ~ expression? ~ (NEWLINE? ~ field_attri type_builder_block = { TYPE_BUILDER_KEYWORD ~ BLOCK_OPEN ~ type_builder_contents ~ BLOCK_CLOSE } -type_builder_contents = { (type_expression_block | type_alias | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* } +type_builder_contents = { (dynamic_type_expression_block | type_expression_block | type_alias | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* } // ###################################### ARROW = { SPACER_TEXT ~ "->" ~ SPACER_TEXT } diff --git a/engine/baml-lib/schema-ast/src/parser/parse_type_builder_block.rs b/engine/baml-lib/schema-ast/src/parser/parse_type_builder_block.rs index 8f857801c..2c7ba63e0 100644 --- a/engine/baml-lib/schema-ast/src/parser/parse_type_builder_block.rs +++ b/engine/baml-lib/schema-ast/src/parser/parse_type_builder_block.rs @@ -58,6 +58,49 @@ pub fn parse_type_builder_contents( match current.as_rule() { Rule::comment_block => pending_block_comment = Some(current), + Rule::dynamic_type_expression_block => { + let dyn_type_expr_span = diagnostics.span(current.as_span()); + + for nested in current.into_inner() { + match nested.as_rule() { + Rule::identifier => { + if nested.as_str() != "dynamic" { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!("Unexpected keyword '{nested}' in dynamic type definition. Use 'dynamic class' or 'dynamic enum'."), + diagnostics.span(nested.as_span()), + )); + } + } + + Rule::type_expression_block => { + let mut type_expr = parse_type_expression_block( + nested, + pending_block_comment.take(), + diagnostics, + ); + + // Include the dynamic keyword in the span. + type_expr.span = dyn_type_expr_span.to_owned(); + + // TODO: #1343 Temporary solution until we implement scoping in the AST. + // We know it's dynamic. The Dynamic subtype will be + // removed later because it's not supported in the + // AST but we store this information here. + type_expr.is_dynamic_type_def = true; + + match type_expr.sub_type { + SubType::Class | SubType::Enum => { + entries.push(TypeBuilderEntry::Dynamic(type_expr)) + } + _ => {} // may need to save other somehow for error propagation + } + } + + _ => parsing_catch_all(nested, "dynamic_type_expression_block"), + } + } + } + Rule::type_expression_block => { let type_expr = parse_type_expression_block(current, pending_block_comment.take(), diagnostics); @@ -65,7 +108,6 @@ pub fn parse_type_builder_contents( match type_expr.sub_type { SubType::Class => entries.push(TypeBuilderEntry::Class(type_expr)), SubType::Enum => entries.push(TypeBuilderEntry::Enum(type_expr)), - SubType::Dynamic => entries.push(TypeBuilderEntry::Dynamic(type_expr)), _ => {} // may need to save other somehow for error propagation } } diff --git a/engine/baml-lib/schema-ast/src/parser/parse_type_expression_block.rs b/engine/baml-lib/schema-ast/src/parser/parse_type_expression_block.rs index 1bf7549fe..0b9ee9dc1 100644 --- a/engine/baml-lib/schema-ast/src/parser/parse_type_expression_block.rs +++ b/engine/baml-lib/schema-ast/src/parser/parse_type_expression_block.rs @@ -38,7 +38,20 @@ pub(crate) fn parse_type_expression_block( match current.as_str() { "class" => sub_type = Some(SubType::Class), "enum" => sub_type = Some(SubType::Enum), - "dynamic" => sub_type = Some(SubType::Dynamic), + + // Since previously this was allowed we will display a + // nice error here for users who have this in their + // codebase. + "dynamic" => { + diagnostics.push_error(DatamodelError::new_validation_error( + &format!( + "Incomplete 'dynamic' type definition. Use 'dynamic class' or 'dynamic enum' to add properties to types that contain the `@@dynamic` attribute.", + ), + diagnostics.span(current.as_span()), + )); + + sub_type = Some(SubType::Other("dynamic".to_string())) + } // Report this as an error, otherwise the syntax will be // correct but the type will not be registered and the @@ -80,7 +93,11 @@ pub(crate) fn parse_type_expression_block( sub_type.clone().map(|st| match st { SubType::Enum => "Enum", SubType::Class => "Class", - SubType::Dynamic => "Dynamic", + SubType::Dynamic(d) => match *d { + SubType::Class => "Dynamic Class", + SubType::Enum => "Dynamic Enum", + _ => "Dynamic Other" + }, SubType::Other(_) => "Other", }).unwrap_or(""), item, @@ -123,9 +140,9 @@ pub(crate) fn parse_type_expression_block( sub_type: sub_type .clone() .unwrap_or(SubType::Other("Subtype not found".to_string())), - is_dynamic_type_def: matches!(sub_type, Some(SubType::Dynamic)), + is_dynamic_type_def: matches!(sub_type, Some(SubType::Dynamic(_))), }, - _ => panic!("Encountered impossible type_expression declaration during parsing",), + _ => panic!("Encountered impossible type_expression declaration during parsing"), } } diff --git a/engine/baml-runtime/tests/test_runtime.rs b/engine/baml-runtime/tests/test_runtime.rs index 2902e088c..cac2f0db2 100644 --- a/engine/baml-runtime/tests/test_runtime.rs +++ b/engine/baml-runtime/tests/test_runtime.rs @@ -674,7 +674,7 @@ test RecursiveAliasCycle { end_date string } - dynamic Resume { + dynamic class Resume { experience Experience[] } } @@ -726,7 +726,7 @@ test RecursiveAliasCycle { test ReturnDynamicEnumTest { functions [ClassifyMessage] type_builder { - dynamic Category { + dynamic enum Category { Question Feedback TechnicalSupport @@ -792,12 +792,12 @@ test RecursiveAliasCycle { Healthcare } - dynamic Role { + dynamic enum Role { ProductManager Sales } - dynamic Resume { + dynamic class Resume { experience Experience[] role Role industry Industry @@ -865,7 +865,7 @@ test RecursiveAliasCycle { type ExpAlias = Experience - dynamic Resume { + dynamic class Resume { experience ExpAlias } } @@ -930,7 +930,7 @@ test RecursiveAliasCycle { type_builder { type JSON = int | float | bool | string | JSON[] | map - dynamic Resume { + dynamic class Resume { experience JSON } } diff --git a/integ-tests/python/tests/test_functions.py b/integ-tests/python/tests/test_functions.py index cc19e4507..5c456fe32 100644 --- a/integ-tests/python/tests/test_functions.py +++ b/integ-tests/python/tests/test_functions.py @@ -1595,7 +1595,7 @@ class ExtraPersonInfo { weight int } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? } @@ -1611,7 +1611,7 @@ class ExtraPersonInfo { async def test_add_baml_existing_enum(): tb = TypeBuilder() tb.add_baml(""" - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } @@ -1635,16 +1635,16 @@ class ExtraPersonInfo { Musician } - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } - dynamic Color { + dynamic enum Color { BROWN } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? job Job? diff --git a/integ-tests/ruby/test_functions.rb b/integ-tests/ruby/test_functions.rb index 5ac77e23f..2cd575f0d 100644 --- a/integ-tests/ruby/test_functions.rb +++ b/integ-tests/ruby/test_functions.rb @@ -377,7 +377,7 @@ class ExtraPersonInfo { weight int } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? } @@ -395,7 +395,7 @@ class ExtraPersonInfo { it "tests add baml existing enum" do tb = Baml::TypeBuilder.new tb.add_baml(" - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } @@ -421,16 +421,16 @@ class ExtraPersonInfo { Musician } - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } - dynamic Color { + dynamic enum Color { BROWN } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? job Job? diff --git a/integ-tests/typescript/tests/dynamic-types.test.ts b/integ-tests/typescript/tests/dynamic-types.test.ts index bedc819e2..e927b3b0a 100644 --- a/integ-tests/typescript/tests/dynamic-types.test.ts +++ b/integ-tests/typescript/tests/dynamic-types.test.ts @@ -86,7 +86,7 @@ describe('Dynamic Type Tests', () => { weight int } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? } @@ -101,7 +101,7 @@ describe('Dynamic Type Tests', () => { it('should add to existing enum', async () => { let tb = new TypeBuilder() tb.addBaml(` - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } @@ -124,16 +124,16 @@ describe('Dynamic Type Tests', () => { Musician } - dynamic Hobby { + dynamic enum Hobby { VideoGames BikeRiding } - dynamic Color { + dynamic enum Color { BROWN } - dynamic Person { + dynamic class Person { age int? extra ExtraPersonInfo? job Job?