From 8bdd1c0f847f2b02f3ebc797182494758de4ed2e Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 19:01:54 -0700 Subject: [PATCH 1/6] remove legacy Jinja prompt support --- baml_language/Cargo.lock | 41 - baml_language/Cargo.toml | 12 - baml_language/architecture/architecture.svg | 3533 +++++++++-------- baml_language/crates/baml_base/src/dedent.rs | 15 +- .../keyword_docs/baml_keywords.yaml | 7 - .../crates/baml_compiler2_ast/src/ast.rs | 10 - .../baml_compiler2_ast/src/lower_cst.rs | 54 +- .../src/lowering_diagnostic.rs | 6 +- .../crates/baml_compiler2_emit/src/lib.rs | 59 +- .../src/item_tree/template_strings.rs | 2 +- .../src/item_data/functions.rs | 37 - .../src/item_data/template_strings.rs | 2 +- .../src/diagnostic.rs | 40 - .../src/errors/type_error.rs | 112 - .../src/to_diagnostic.rs | 244 -- .../crates/baml_compiler_lexer/src/tokens.rs | 2 +- .../crates/baml_compiler_parser/src/parser.rs | 189 +- .../crates/baml_compiler_syntax/src/ast.rs | 50 +- .../baml_compiler_syntax/src/syntax_kind.rs | 7 - baml_language/crates/baml_fmt/src/lib.rs | 24 +- .../crates/baml_lsp2_actions/Cargo.toml | 1 - .../crates/baml_lsp2_actions/src/actions.rs | 2 +- .../baml_lsp2_actions/src/annotations.rs | 2 +- .../crates/baml_lsp2_actions/src/check.rs | 702 +--- .../baml_lsp2_actions/src/completions.rs | 12 - .../crates/baml_lsp2_actions/src/type_info.rs | 10 +- .../src/lsp_issues_authoring.rs | 1 - .../completion/top_level_keywords.baml | 2 +- .../semantic_tokens/template_string.baml | 58 +- .../template_string_calls.baml | 83 +- .../syntax/client/remap_role_valid.baml | 24 +- .../remap_role_with_default_fallback.baml | 24 +- .../syntax/dictionary/valid_dictionary.baml | 4 +- .../functions_v2/prompt_errors/prompt1.baml | 18 +- .../syntax/template_string/bad_calls.baml | 131 +- .../syntax/template_string/good_calls.baml | 18 +- .../syntax/template_string/invalid.baml | 50 +- .../syntax/template_string/valid.baml | 12 +- .../crates/baml_project/src/client_codegen.rs | 2 +- baml_language/crates/baml_project/src/db.rs | 8 +- .../crates/baml_surface/src/handles_tests.rs | 1 - .../header_in_llm_function/test.baml | 8 +- .../ambiguous_function.baml | 2 +- .../parser_speculative/llm_function.baml | 6 +- .../parser_speculative/mixed_functions.baml | 12 +- .../parser_strings/nested_quotes.baml | 8 +- .../parser_strings/raw_strings.baml | 6 +- .../parser_strings/unicode_strings.baml | 6 +- .../pending_greaters_fix/extra_greater.baml | 4 +- .../template_string_removed/main.baml | 1 + .../compiles/backtick_decl_slots/main.baml | 15 +- .../string_methods/string_methods.baml | 12 +- .../client_option_types.baml | 8 +- .../format_checks/config_decls.baml | 2 +- .../format_checks/template_string_decls.baml | 438 +- .../jinja_prompt_removed/main.baml | 4 + .../unsupported_contexts.baml | 4 +- .../projects/parser_stress/large_file.baml | 200 +- ...eader_in_llm_function__05_diagnostics.snap | 24 +- ...x__parser_speculative__05_diagnostics.snap | 65 +- ...yntax__parser_strings__05_diagnostics.snap | 109 +- ..._pending_greaters_fix__05_diagnostics.snap | 28 +- ...mplate_string_removed__05_diagnostics.snap | 12 + ...ompiles__backtick_decl_slots__03_ppir.snap | 3 + ...mpiles__backtick_decl_slots__04_5_mir.snap | 22 + ...compiles__backtick_decl_slots__04_tir.snap | 5 + ...iles__backtick_decl_slots__06_codegen.snap | 11 + ...cktick_decl_slots__10_formatter__main.snap | 15 +- ...ts__compiles__string_methods__03_ppir.snap | 13 +- ...s__compiles__string_methods__04_5_mir.snap | 89 + ...sts__compiles__string_methods__04_tir.snap | 20 + ..._compiles__string_methods__06_codegen.snap | 41 + ...methods__10_formatter__string_methods.snap | 20 +- ..._errors__client_option_types__03_ppir.snap | 17 +- ...c_errors__client_option_types__04_tir.snap | 17 + ...__client_option_types__05_diagnostics.snap | 11 - ...es__10_formatter__client_option_types.snap | 8 +- ...nostic_errors__format_checks__03_ppir.snap | 52 +- ...gnostic_errors__format_checks__04_tir.snap | 118 + ...errors__format_checks__05_diagnostics.snap | 404 +- ...at_checks__10_formatter__config_decls.snap | 2 +- ...__10_formatter__template_string_decls.snap | 348 +- ...errors__jinja_prompt_removed__03_ppir.snap | 5 + ..._errors__jinja_prompt_removed__04_tir.snap | 4 + ..._jinja_prompt_removed__05_diagnostics.snap | 12 + ...ja_prompt_removed__10_formatter__main.snap | 7 + ..._optional_parameter_defaults__03_ppir.snap | 4 +- ...__optional_parameter_defaults__04_tir.snap | 9 +- ...al_parameter_defaults__05_diagnostics.snap | 25 +- ...s__10_formatter__unsupported_contexts.snap | 4 +- .../crates/baml_tests/src/compiler2_hir.rs | 8 +- .../baml_tests/src/incremental/scenarios.rs | 22 +- .../tests/backtick_fmt_value_preservation.rs | 4 +- .../tests/convert_boundary_regressions.rs | 2 +- .../tests/incremental_typing_repro.rs | 24 +- .../crates/baml_tests/tests/interfaces.rs | 44 +- .../crates/baml_tests/tests/prompt_tag_e2e.rs | 3 - baml_language/crates/baml_type/src/defs.rs | 7 +- baml_language/crates/bex_engine/src/lib.rs | 12 +- .../crates/bex_engine/tests/llm_render.rs | 259 +- baml_language/crates/bex_vm/src/vm.rs | 3 - baml_language/crates/bex_vm_types/src/link.rs | 12 - .../crates/bex_vm_types/src/types.rs | 4 - .../crates/bex_vm_types/src/types/function.rs | 5 +- baml_language/crates/bex_vm_types/src/unit.rs | 2 - baml_language/crates/sys_jinja/Cargo.toml | 16 - baml_language/crates/sys_jinja/src/lib.rs | 106 - .../crates/sys_jinja_types/Cargo.toml | 16 - .../sys_jinja_types/src/evaluate_type/expr.rs | 930 ----- .../sys_jinja_types/src/evaluate_type/mod.rs | 465 --- .../src/evaluate_type/pretty_print.rs | 153 - .../sys_jinja_types/src/evaluate_type/stmt.rs | 516 --- .../src/evaluate_type/test_expr.rs | 550 --- .../src/evaluate_type/test_stmt.rs | 285 -- .../src/evaluate_type/types.rs | 813 ---- .../crates/sys_jinja_types/src/lib.rs | 297 -- baml_language/crates/sys_llm/Cargo.toml | 2 - .../crates/sys_llm/src/jinja/error.rs | 19 - baml_language/crates/sys_llm/src/jinja/mod.rs | 26 - .../sys_llm/src/jinja/output_format_object.rs | 218 - .../crates/sys_llm/src/jinja/render.rs | 860 ---- .../sys_llm/src/jinja/value_conversion.rs | 128 - baml_language/crates/sys_llm/src/lib.rs | 67 +- baml_language/crates/sys_types/src/lib.rs | 13 - 124 files changed, 3361 insertions(+), 10401 deletions(-) create mode 100644 baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml create mode 100644 baml_language/crates/baml_tests/projects/diagnostic_errors/jinja_prompt_removed/main.baml create mode 100644 baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__03_ppir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__04_tir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__10_formatter__main.snap delete mode 100644 baml_language/crates/sys_jinja/Cargo.toml delete mode 100644 baml_language/crates/sys_jinja/src/lib.rs delete mode 100644 baml_language/crates/sys_jinja_types/Cargo.toml delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/expr.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/mod.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/pretty_print.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/stmt.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/test_expr.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/test_stmt.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/evaluate_type/types.rs delete mode 100644 baml_language/crates/sys_jinja_types/src/lib.rs delete mode 100644 baml_language/crates/sys_llm/src/jinja/error.rs delete mode 100644 baml_language/crates/sys_llm/src/jinja/mod.rs delete mode 100644 baml_language/crates/sys_llm/src/jinja/output_format_object.rs delete mode 100644 baml_language/crates/sys_llm/src/jinja/render.rs delete mode 100644 baml_language/crates/sys_llm/src/jinja/value_conversion.rs diff --git a/baml_language/Cargo.lock b/baml_language/Cargo.lock index 413fb0a0b35..43090f4a406 100644 --- a/baml_language/Cargo.lock +++ b/baml_language/Cargo.lock @@ -1030,7 +1030,6 @@ dependencies = [ "rowan 0.16.1", "salsa", "serde", - "sys_jinja_types", "text-size", ] @@ -4580,24 +4579,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "minijinja" -version = "2.16.0" -source = "git+https://github.com/boundaryml/minijinja.git?branch=value-cmp#8cfc770a5dffeda2de5b910d2b9f870d7edeff7c" -dependencies = [ - "indexmap", - "serde", -] - -[[package]] -name = "minijinja-contrib" -version = "2.16.0" -source = "git+https://github.com/boundaryml/minijinja.git?branch=value-cmp#8cfc770a5dffeda2de5b910d2b9f870d7edeff7c" -dependencies = [ - "minijinja", - "serde", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -7315,26 +7296,6 @@ dependencies = [ "regex", ] -[[package]] -name = "sys_jinja" -version = "0.0.0-beta" -dependencies = [ - "minijinja", - "minijinja-contrib", - "regex", - "thiserror 2.0.18", -] - -[[package]] -name = "sys_jinja_types" -version = "0.0.0-beta" -dependencies = [ - "baml_base", - "indexmap", - "minijinja", - "strsim", -] - [[package]] name = "sys_llm" version = "0.0.0-beta" @@ -7357,14 +7318,12 @@ dependencies = [ "getrandom 0.3.4", "indexmap", "infer", - "minijinja", "num-bigint", "rsa", "rustls", "serde", "serde_json", "strum", - "sys_jinja", "sys_types", "thiserror 2.0.18", "tokio", diff --git a/baml_language/Cargo.toml b/baml_language/Cargo.toml index 4d2107fa1a3..fc60e77eca9 100644 --- a/baml_language/Cargo.toml +++ b/baml_language/Cargo.toml @@ -105,8 +105,6 @@ bex_cache = { path = "crates/bex_cache", default-features = false } bex_project = { path = "crates/bex_project", default-features = false } bex_engine = { path = "crates/bex_engine", default-features = false } sys_glob = { path = "crates/sys_glob" } -sys_jinja = { path = "crates/sys_jinja" } -sys_jinja_types = { path = "crates/sys_jinja_types" } sys_llm = { path = "crates/sys_llm", default-features = false } sys_ops = { path = "crates/sys_ops", default-features = false } sys_types = { path = "crates/sys_types" } @@ -135,16 +133,6 @@ bex_resource_types = { path = "crates/bex_resource_types" } darwin-kperf = "0.1.0" darwin-kperf-events = "0.1.0" -# External dependency for sys_llm jinja module -minijinja = { git = "https://github.com/boundaryml/minijinja.git", branch = "value-cmp", default-features = false, features = [ - "macros", - "builtins", - "debug", - "preserve_order", - "serde", - "unstable_machinery", -] } -minijinja-contrib = { git = "https://github.com/boundaryml/minijinja.git", branch = "value-cmp", features = [ "pycompat" ] } baml_workspace = { path = "crates/baml_workspace" } # External dependencies diff --git a/baml_language/architecture/architecture.svg b/baml_language/architecture/architecture.svg index 5f5ca1c0857..f77a559d595 100644 --- a/baml_language/architecture/architecture.svg +++ b/baml_language/architecture/architecture.svg @@ -1,2936 +1,3021 @@ - - + dependencies - + cluster_baml - -baml + +baml cluster_baml_lsp - -baml_lsp + +baml_lsp cluster_bex - -bex + +bex cluster_bridge - -bridge + +bridge cluster_forked_aws - -forked_aws + +forked_aws cluster_forked_google - -forked_google + +forked_google cluster_sdkgen - -sdkgen + +sdkgen cluster_surface - -surface + +surface cluster_sys - -sys + +sys cluster_tools - -tools + +tools baml_base - -baml_base + +baml_base - + baml_salsa - -salsa + +salsa baml_base->baml_salsa - - + + baml_bridge - -baml_bridge + +baml_bridge - + baml_serde_json - -serde_json + +serde_json baml_bridge->baml_serde_json - - + + - + baml_tokio_83a17d2e - -tokio -[macros, rt, rt-multi-thread] + +tokio +[macros, rt, rt-multi-thread] baml_bridge->baml_tokio_83a17d2e - - + + baml_builtins2 - -baml_builtins2 + +baml_builtins2 baml_builtins2->baml_base - - + + baml_builtins2->baml_serde_json - - + + baml_builtins2_codegen - -baml_builtins2_codegen + +baml_builtins2_codegen baml_builtins2_codegen->baml_builtins2 - - + + baml_compiler2_ast - -baml_compiler2_ast + +baml_compiler2_ast baml_builtins2_codegen->baml_compiler2_ast - - + + baml_compiler_parser - -baml_compiler_parser + +baml_compiler_parser baml_builtins2_codegen->baml_compiler_parser - - + + baml_codegen_types - -baml_codegen_types + +baml_codegen_types - + baml_type - -baml_type + +baml_type - + baml_codegen_types->baml_type - - + + + + + +baml_codegen_types->baml_serde_json + + baml_compiler_diagnostics - -baml_compiler_diagnostics + +baml_compiler_diagnostics - + baml_compiler2_ast->baml_compiler_diagnostics - - + + baml_compiler_syntax - -baml_compiler_syntax + +baml_compiler_syntax - + baml_compiler2_ast->baml_compiler_syntax - - + + baml_compiler2_emit - -baml_compiler2_emit + +baml_compiler2_emit baml_compiler2_mir - -baml_compiler2_mir + +baml_compiler2_mir - + baml_compiler2_emit->baml_compiler2_mir - - + + - + bex_vm_types - -bex_vm_types + +bex_vm_types - + baml_compiler2_emit->bex_vm_types - - + + baml_compiler2_hir - -baml_compiler2_hir + +baml_compiler2_hir - + baml_compiler2_hir->baml_compiler2_ast - - + + - + baml_compiler2_hir->baml_compiler_parser - - + + - + baml_workspace - -baml_workspace + +baml_workspace - + baml_compiler2_hir->baml_workspace - - + + baml_compiler2_tir - -baml_compiler2_tir + +baml_compiler2_tir - + baml_compiler2_mir->baml_compiler2_tir - - + + baml_compiler2_ppir - -baml_compiler2_ppir + +baml_compiler2_ppir - + baml_compiler2_ppir->baml_compiler2_hir - - + + - + baml_compiler2_tir->baml_builtins2 - - + + - + baml_compiler2_tir->baml_compiler2_ppir - - + + - + baml_type_runtime - -baml_type_runtime + +baml_type_runtime - + baml_compiler2_tir->baml_type_runtime - - + + baml_compiler2_visualization - -baml_compiler2_visualization + +baml_compiler2_visualization - + baml_compiler2_visualization->baml_compiler2_ast - - + + - + baml_compiler_diagnostics->baml_base - - + + baml_compiler_lexer - -baml_compiler_lexer + +baml_compiler_lexer - + baml_compiler_lexer->baml_base - - + + - + baml_compiler_parser->baml_compiler_diagnostics - - + + - + baml_compiler_parser->baml_compiler_lexer - - + + - + baml_compiler_parser->baml_compiler_syntax - - + + - + baml_compiler_syntax->baml_base - - + + - + baml_rowan - -rowan + +rowan - + baml_compiler_syntax->baml_rowan - - + + baml_db - -baml_db + +baml_db - + baml_db->baml_compiler2_emit - - + + baml_fmt - -baml_fmt + +baml_fmt baml_project - -baml_project + +baml_project - + baml_fmt->baml_project - - + + - + baml_fmt->baml_rowan - - + + baml_lsp2_actions - -baml_lsp2_actions + +baml_lsp2_actions - + baml_lsp2_actions->baml_compiler2_tir - - - - - -sys_jinja_types - -sys_jinja_types - - - -baml_lsp2_actions->sys_jinja_types - - + + - + baml_project->baml_codegen_types - - + + - + baml_project->baml_compiler2_visualization - - + + - + baml_project->baml_db - - + + - + baml_project->baml_lsp2_actions - - + + + + + +baml_surface + +baml_surface + + + +baml_project->baml_surface + + - + baml_project->bex_vm_types - - + + baml_release - -baml_release + +baml_release - + baml_anyhow - -anyhow + +anyhow - + baml_release->baml_anyhow - - + + - + baml_reqwest_fd3956d6 - -reqwest -[blocking, rustls] + +reqwest +[blocking, rustls] - + baml_release->baml_reqwest_fd3956d6 - - + + - + baml_release->baml_serde_json - - + + + + + +baml_surface->baml_compiler2_tir + + - + baml_type->baml_base - - + + - + baml_type_macros - -baml_type_macros + +baml_type_macros - + baml_type->baml_type_macros - - + + - + baml_type_runtime->baml_type - - + + - + baml_version - -baml_version + +baml_version - + baml_workspace->baml_type - - + + - + proxy_baml_bex_project - -bex_project + +bex_project - + proxy_baml_bex_project->baml_builtins2 - - + + - + proxy_baml_bex_project->baml_compiler2_visualization - - + + - + proxy_baml_bex_project->baml_compiler_diagnostics - - + + - + proxy_baml_bex_project->baml_fmt - - + + - + proxy_baml_bex_project->baml_lsp2_actions - - + + - + proxy_baml_bex_project->baml_project - - + + - + proxy_baml_bex_project->baml_type - - + + - + proxy_baml_bex_project->baml_version - - + + - + proxy_baml_bex_project->baml_workspace - - + + - + baml_lsp_server - -baml_lsp_server + +baml_lsp_server - + baml_lsp_server->baml_compiler2_visualization - - + + - + baml_lsp_server->baml_version - - + + - + baml_lsp_anyhow - -anyhow + +anyhow - + baml_lsp_server->baml_lsp_anyhow - - + + - + baml_lsp_reqwest - -reqwest + +reqwest - + baml_lsp_server->baml_lsp_reqwest - - + + - + baml_lsp_serde_json - -serde_json + +serde_json - + baml_lsp_server->baml_lsp_serde_json - - + + - + baml_lsp_tokio_e2f85805 - -tokio -[macros, net, rt-multi-thread, sync, time] + +tokio +[macros, net, rt-multi-thread, sync, time] - + baml_lsp_server->baml_lsp_tokio_e2f85805 - - + + - + bex_events - -bex_events + +bex_events - + baml_lsp_server->bex_events - - + + - + bex_heap - -bex_heap + +bex_heap - + baml_lsp_server->bex_heap - - + + - + bex_project - -bex_project + +bex_project - + baml_lsp_server->bex_project - - + + - + bridge_ctypes - -bridge_ctypes + +bridge_ctypes - + baml_lsp_server->bridge_ctypes - - + + - + sys_native - -sys_native + +sys_native - + baml_lsp_server->sys_native - - + + - + sys_ops - -sys_ops + +sys_ops - + baml_lsp_server->sys_ops - - + + - + sys_types - -sys_types + +sys_types - + baml_lsp_server->sys_types - - + + - + bex_cache - -bex_cache + +bex_cache - + bex_cache->baml_type - - + + - + bex_cache->baml_version - - + + - + bex_cache->bex_vm_types - - + + - - -bex_reqwest_fd3956d6 - -reqwest -[blocking, rustls] + + +bex_reqwest_d8636c35 + +reqwest +[blocking] - - -bex_cache->bex_reqwest_fd3956d6 - - + + +bex_cache->bex_reqwest_d8636c35 + + - + bex_engine - -bex_engine + +bex_engine - + bex_engine->baml_builtins2 - - + + - + bex_engine->baml_type - - + + - + bex_engine->baml_type_runtime - - + + - + bex_vm - -bex_vm + +bex_vm - + bex_engine->bex_vm - - + + - + proxy_bex_sys_types - -sys_types + +sys_types - + bex_engine->proxy_bex_sys_types - - + + - - -bex_tokio_f0c4432d - -tokio -[macros, rt, rt-multi-thread, sync] + + +bex_tokio_27fa7540 + +tokio +[macros, rt, rt-multi-thread, sync, time] - - -bex_engine->bex_tokio_f0c4432d - - + + +bex_engine->bex_tokio_27fa7540 + + - + bex_engine->sys_ops - - + + - + bex_external_types - -bex_external_types + +bex_external_types - + bex_events->bex_external_types - - + + - + bex_events->proxy_bex_sys_types - - + + - + bex_serde_json - -serde_json + +serde_json - + bex_events->bex_serde_json - - + + - + bex_external_types->baml_builtins2 - - + + - + bex_external_types->baml_type - - + + - + bex_external_types->bex_vm_types - - + + - + bex_heap->baml_type - - + + - + bex_heap->bex_external_types - - + + - + bex_tokio_2d49ec94 - -tokio -[macros, rt-multi-thread, sync, time] + +tokio +[macros, rt-multi-thread, sync, time] - + bex_heap->bex_tokio_2d49ec94 - - + + - + bex_project->proxy_baml_bex_project - - + + - + bex_project->bex_cache - - + + - + bex_project->bex_engine - - + + - + bex_project->proxy_bex_sys_types - - + + - + bex_tokio_6f91154 - -tokio -[rt, sync, time] + +tokio +[rt, sync, time] - + bex_project->bex_tokio_6f91154 - - + + - + bex_project->sys_ops - - + + - + bex_resource_types - -bex_resource_types + +bex_resource_types - + bex_sap - -bex_sap + +bex_sap - + bex_sap->baml_type - - + + - + bex_sap->bex_external_types - - + + - + bex_sap->proxy_bex_sys_types - - + + - + bex_sap->bex_serde_json - - + + - + bex_str - -bex_str + +bex_str - + bex_vm->baml_builtins2 - - + + - + bex_vm->baml_type - - + + - + bex_vm->bex_events - - + + - + bex_vm->bex_heap - - + + - + bex_tokio_3135f4fb - -tokio -[macros, rt] + +tokio +[macros, rt] - + bex_vm->bex_tokio_3135f4fb - - + + - + bex_vm_types->baml_builtins2 - - + + - + bex_vm_types->baml_type - - + + - + bex_vm_types->bex_resource_types - - + + - + bex_vm_types->bex_str - - + + - + bex_tokio_f5bec500 - -tokio -[macros, rt, sync] + +tokio +[macros, rt, sync] - + bex_vm_types->bex_tokio_f5bec500 - - + + - + proxy_bex_sys_types->sys_types - - + + - + bridge_cffi - -bridge_cffi + +bridge_cffi - + bridge_cffi->baml_version - - + + - + bridge_cffi->bex_events - - + + - + bridge_cffi->bridge_ctypes - - + + - + proxy_bridge_bex_project - -bex_project + +bex_project - + bridge_cffi->proxy_bridge_bex_project - - + + - + proxy_bridge_sys_native - -sys_native + +sys_native - + bridge_cffi->proxy_bridge_sys_native - - + + - + proxy_bridge_sys_types - -sys_types + +sys_types - + bridge_cffi->proxy_bridge_sys_types - - + + - + bridge_serde_json - -serde_json + +serde_json - + bridge_cffi->bridge_serde_json - - + + - + bridge_tokio_8e1a8cc7 - -tokio -[rt, rt-multi-thread] + +tokio +[rt, rt-multi-thread] - + bridge_cffi->bridge_tokio_8e1a8cc7 - - + + - + bridge_cffi->sys_ops - - + + - + bridge_ctypes->baml_type - - + + - + bridge_ctypes->proxy_bridge_bex_project - - + + - + bridge_java - -bridge_java - - - -bridge_java->baml_version - - + +bridge_java - + bridge_java->bridge_cffi - - + + - + bridge_java->proxy_bridge_bex_project - - + + - + bridge_python - -bridge_python + +bridge_python - + bridge_python->baml_version - - + + - + bridge_python->bex_events - - + + - + bridge_python->bridge_cffi - - + + - + bridge_python->proxy_bridge_bex_project - - + + - + bridge_python->proxy_bridge_sys_native - - + + + + + +bridge_swift + +bridge_swift + + + +bridge_swift->bridge_cffi + + - + bridge_typescript - -bridge_typescript + +bridge_typescript - + bridge_typescript->baml_version - - + + - + bridge_typescript->bex_events - - + + - + bridge_typescript->bridge_cffi - - + + - + bridge_typescript->proxy_bridge_bex_project - - + + - + bridge_typescript->proxy_bridge_sys_native - - + + - + bridge_typescript_web - -bridge_typescript_web + +bridge_typescript_web - + bridge_typescript_web->baml_version - - + + - + bridge_typescript_web->bridge_cffi - - + + - + bridge_typescript_web->proxy_bridge_bex_project - - + + - + sys_wasm - -sys_wasm + +sys_wasm - + bridge_typescript_web->sys_wasm - - + + - + bridge_wasm - -bridge_wasm + +bridge_wasm - + bridge_wasm->baml_type - - + + - + bridge_wasm->baml_version - - + + - + bridge_wasm->bex_events - - + + - + bridge_wasm->bex_heap - - + + - + bridge_wasm->bridge_ctypes - - + + - + bridge_wasm->proxy_bridge_bex_project - - + + - + bridge_wasm->proxy_bridge_sys_types - - + + - + bridge_reqwest - -reqwest + +reqwest - + bridge_wasm->bridge_reqwest - - + + - + bridge_wasm->bridge_serde_json - - + + - + sys_glob - -sys_glob + +sys_glob - + bridge_wasm->sys_glob - - + + - + bridge_wasm->sys_ops - - + + - + bridge_wasm->sys_wasm - - + + - + proxy_bridge_bex_project->bex_project - - + + - + proxy_bridge_sys_native->sys_native - - + + - + proxy_bridge_sys_types->sys_types - - + + - + forked_aws_bedrock - -forked_aws_bedrock + +forked_aws_bedrock - + forked_aws_serde_json - -serde_json + +serde_json - + forked_aws_bedrock->forked_aws_serde_json - - + + - + forked_aws_config - -forked_aws_config + +forked_aws_config - + forked_aws_sigv4 - -forked_aws_sigv4 + +forked_aws_sigv4 - + forked_aws_config->forked_aws_sigv4 - - + + - + forked_aws_config->forked_aws_serde_json - - + + - + forked_google_cloud_auth - -forked_google_cloud_auth + +forked_google_cloud_auth - + forked_google_serde_json - -serde_json + +serde_json - + forked_google_cloud_auth->forked_google_serde_json - - + + - + sdkgen_cpp - -sdkgen_cpp + +sdkgen_cpp - + sdkgen_cpp->baml_base - - + + - + sdkgen_cpp->baml_codegen_types - - + + - + sdkgen_cpp->baml_version - - + + + + + +sdkgen_csharp + +sdkgen_csharp + + + +sdkgen_csharp->baml_base + + + + + +sdkgen_csharp->baml_codegen_types + + + + + +sdkgen_csharp->bex_vm_types + + + + + +sdkgen_serde_json + +serde_json + + + +sdkgen_csharp->sdkgen_serde_json + + - + sdkgen_go - -sdkgen_go + +sdkgen_go - + sdkgen_go->baml_codegen_types - - + + - + sdkgen_go->baml_type - - + + - + sdkgen_java - -sdkgen_java + +sdkgen_java - + sdkgen_java->baml_base - - + + - + sdkgen_java->baml_codegen_types - - + + - + sdkgen_python_pydantic2 - -sdkgen_python_pydantic2 + +sdkgen_python_pydantic2 - + sdkgen_python_pydantic2->baml_base - - + + - + sdkgen_python_pydantic2->baml_codegen_types - - + + - + sdkgen_rust - -sdkgen_rust + +sdkgen_rust - + sdkgen_rust->baml_base - - + + - + sdkgen_rust->baml_codegen_types - - + + + + + +sdkgen_swift + +sdkgen_swift + + + +sdkgen_swift->baml_base + + + + + +sdkgen_swift->baml_codegen_types + + + + + +sdkgen_swift->baml_version + + - + sdkgen_typescript_shared - -sdkgen_typescript_shared + +sdkgen_typescript_shared - + sdkgen_typescript_shared->baml_base - - + + - + sdkgen_typescript_shared->baml_codegen_types - - + + - + baml - -baml + +baml baml->baml_release - - + + - + surface_anyhow - -anyhow + +anyhow baml->surface_anyhow - - + + - + surface_reqwest_fd3956d6 - -reqwest -[blocking, rustls] + +reqwest +[blocking, rustls] baml->surface_reqwest_fd3956d6 - - + + - + surface_serde_json - -serde_json + +serde_json baml->surface_serde_json - - + + - + baml_cli - -baml_cli + +baml_cli baml_cli->baml_fmt - - + + baml_cli->baml_release - - + + baml_cli->baml_version - - + + baml_cli->baml_lsp_server - - + + baml_cli->bex_cache - - + + baml_cli->bex_engine - - + + baml_cli->bex_vm_types - - + + baml_cli->sdkgen_cpp - - + + - + +baml_cli->sdkgen_csharp + + + + + baml_cli->sdkgen_go - - + + - + baml_cli->sdkgen_java - - + + - + baml_cli->sdkgen_python_pydantic2 - - + + - + baml_cli->sdkgen_rust - - + + + + + +baml_cli->sdkgen_swift + + - + baml_cli->sdkgen_typescript_shared - - + + - + baml_exec - -baml_exec + +baml_exec baml_cli->baml_exec - - + + - - -surface_clap_5a72f63 - -clap -[color] + + +surface_clap_36a4cd6c + +clap +[color, env, wrap_help] - + -baml_cli->surface_clap_5a72f63 - - +baml_cli->surface_clap_36a4cd6c + + - + surface_reqwest_1c741f78 - -reqwest -[blocking, json, rustls] + +reqwest +[blocking, json, rustls] baml_cli->surface_reqwest_1c741f78 - - + + - + surface_tokio - -tokio + +tokio - + baml_cli->surface_tokio - - + + - + baml_cli->sys_native - - + + - + baml_exec->baml_type - - + + - + baml_exec->bex_engine - - + + - + baml_exec->bex_vm_types - - + + - + baml_exec->surface_anyhow - - + + - + surface_clap - -clap + +clap - + baml_exec->surface_clap - - + + - + baml_exec->surface_serde_json - - + + - + baml_pack_host - -baml_pack_host + +baml_pack_host - + baml_pack_host->bex_engine - - + + - + baml_pack_host->bex_events - - + + - + baml_pack_host->baml_exec - - + + - + surface_tokio_e42 - -tokio -[rt] + +tokio +[rt] - + baml_pack_host->surface_tokio_e42 - - + + - + baml_pack_host->sys_native - - - - - -sys_jinja - -sys_jinja - - - -sys_jinja_types->baml_base - - + + - + sys_llm - -sys_llm + +sys_llm - + sys_llm->baml_builtins2 - - + + - + sys_llm->bex_sap - - + + - + sys_llm->forked_aws_bedrock - - + + - + sys_llm->forked_aws_config - - + + - + sys_llm->forked_aws_sigv4 - - + + - + sys_llm->forked_google_cloud_auth - - - - - -sys_llm->sys_jinja - - + + - + sys_llm->sys_types - - + + - + proxy_sys_baml_type - -baml_type + +baml_type - + sys_llm->proxy_sys_baml_type - - + + - + proxy_sys_bex_heap - -bex_heap + +bex_heap - + sys_llm->proxy_sys_bex_heap - - + + - + sys_serde_json - -serde_json + +serde_json - + sys_llm->sys_serde_json - - + + - + sys_tokio_83a17d2e - -tokio -[macros, rt, rt-multi-thread] + +tokio +[macros, rt, rt-multi-thread] - + sys_llm->sys_tokio_83a17d2e - - + + - + sys_native->bex_resource_types - - + + - + sys_native->bridge_ctypes - - + + - + sys_native->sys_glob - - + + - + sys_native->sys_ops - - + + - + sys_native->proxy_sys_baml_type - - + + - + sys_native->proxy_sys_bex_heap - - + + - + sys_reqwest - -reqwest + +reqwest - + sys_native->sys_reqwest - - - - - -sys_native->sys_serde_json - - + + - + sys_tokio_41f3dede - -tokio -[fs, io-std, io-util, net, process, sync, time] + +tokio +[fs, io-std, io-util, net, process, sync, time] - + sys_native->sys_tokio_41f3dede - - + + - + sys_ops->baml_builtins2 - - + + - + sys_ops->bex_sap - - + + - + sys_ops->sys_llm - - + + - + sys_ops->proxy_sys_baml_type - - + + - + sys_ops->proxy_sys_bex_heap - - + + - + sys_types->bex_str - - + + - + sys_types->proxy_sys_baml_type - - + + - + sys_types->proxy_sys_bex_heap - - + + - + sys_tokio_f5bec500 - -tokio -[macros, rt, sync] + +tokio +[macros, rt, sync] - + sys_types->sys_tokio_f5bec500 - - + + - + sys_wasm->bex_project - - + + - + sys_wasm->bridge_ctypes - - + + - + sys_wasm->sys_ops - - + + - + sys_wasm->proxy_sys_baml_type - - + + - + proxy_sys_baml_type->baml_type - - + + - + proxy_sys_bex_heap->bex_heap - - + + - + cargo_size_gate - -cargo-size-gate + +cargo-size-gate - + tools_anyhow - -anyhow + +anyhow - + cargo_size_gate->tools_anyhow - - + + - + tools_clap - -clap + +clap - + cargo_size_gate->tools_clap - - + + - + tools_serde_json - -serde_json + +serde_json - + cargo_size_gate->tools_serde_json - - + + - + cargo_stow - -cargo-stow + +cargo-stow - + cargo_stow->tools_clap - - + + - + tools_compile_profile - -tools_compile_profile + +tools_compile_profile - + tools_compile_profile->baml_compiler_diagnostics - - + + - + tools_compile_profile->baml_project - - + + - + tools_compile_profile->baml_workspace - - + + - + tools_compile_profile->tools_anyhow - - + + - + tools_compile_profile->tools_clap - - + + - + tools_salsa - -salsa + +salsa - + tools_compile_profile->tools_salsa - - + + - + tools_compile_profile->tools_serde_json - - + + - + tools_sap_visualizer - -tools_sap_visualizer + +tools_sap_visualizer - + tools_sap_visualizer->baml_project - - + + - + tools_sap_visualizer->baml_type - - + + - + tools_sap_visualizer->bex_sap - - + + - + tools_sap_visualizer->bex_vm_types - - + + - + tools_sap_visualizer->sys_types - - + + - + tools_sap_visualizer->tools_serde_json - - + + - + tools_sccache - -tools_sccache + +tools_sccache - + tools_semantic_tokens - -tools_semantic_tokens + +tools_semantic_tokens - + tools_semantic_tokens->baml_lsp2_actions - - + + - + tools_semantic_tokens->baml_project - - + + - + tools_semantic_tokens->tools_anyhow - - + + - + tools_semantic_tokens->tools_clap - - + + - + tools_semantic_tokens->tools_serde_json - - + + - + tools_tokio_f5ba88ac - -tokio -[macros, net, rt] + +tokio +[macros, net, rt] - + tools_semantic_tokens->tools_tokio_f5ba88ac - - + + - + reqwest - -reqwest + +reqwest - + external_serde_json - -external_serde_json + +external_serde_json - + reqwest->external_serde_json - - + + - + external_tokio - -external_tokio + +external_tokio - + reqwest->external_tokio - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/baml_language/crates/baml_base/src/dedent.rs b/baml_language/crates/baml_base/src/dedent.rs index 367363d1c49..9f238905640 100644 --- a/baml_language/crates/baml_base/src/dedent.rs +++ b/baml_language/crates/baml_base/src/dedent.rs @@ -1,22 +1,15 @@ -//! Template-string dedenting. +//! Backtick-string dedenting. //! -//! Shared with `sys_llm::jinja::render::preprocess_template`; both implement the -//! same algorithm and should remain in lockstep until that callsite migrates to -//! this one. -//! -//! Algorithm (BEP-049 §12, Kotlin `trimIndent` rule): +//! Algorithm (BEP-049 section 12, Kotlin `trimIndent` rule): //! 1. Compute the longest common leading-whitespace *prefix* across all non-blank //! lines, compared character-by-character. Tabs and spaces don't mix: a //! tab-indented line and a space-indented line share no common prefix, so the -//! strip column drops to zero (§12 Rule 2). +//! strip column drops to zero (section 12, rule 2). //! 2. Strip that common prefix from every line that has it; otherwise keep the //! line as-is (its content is whitespace-only). //! 3. Trim leading/trailing whitespace from the overall result. //! -//! Used by BEP-049 backtick string literals (multi-line auto-dedent, §12). The -//! legacy Jinja prompt pipeline (`sys_llm::preprocess_template`) keeps the older -//! byte-count-min variant until that path is removed (M6) — they intentionally -//! diverge on mixed tab/space indentation, which only the new backtick form specs. +//! Used by BEP-049 backtick string literals for multi-line auto-dedent. // Walk leading whitespace by *char* so we never split a multi-byte // Unicode whitespace codepoint (NBSP U+00A0 = 2 bytes, LINE SEPARATOR // U+2028 = 3 bytes, etc.). A naive `line.len() - line.trim_start().len()` diff --git a/baml_language/crates/baml_builtins2/keyword_docs/baml_keywords.yaml b/baml_language/crates/baml_builtins2/keyword_docs/baml_keywords.yaml index e3a5abe02cf..3c60fa75b75 100644 --- a/baml_language/crates/baml_builtins2/keyword_docs/baml_keywords.yaml +++ b/baml_language/crates/baml_builtins2/keyword_docs/baml_keywords.yaml @@ -320,13 +320,6 @@ testset: Every leaf gets a canonical ID beginning with `root` (or `root.`) and using `::` for testset nesting. For example, the first test above is `root::math suite::adds`. Run `baml test --list` to discover IDs and `baml test --help` for selectors and profiles. -template_string: - summary: "Defines a reusable prompt template with Jinja2 syntax." - syntax: | - template_string Greeting(name: string) #" - Hello, {{ name }}! How can I help you? - "# - retry_policy: summary: "Configures automatic retry behavior for LLM calls." syntax: | diff --git a/baml_language/crates/baml_compiler2_ast/src/ast.rs b/baml_language/crates/baml_compiler2_ast/src/ast.rs index 2952a206eaa..bfda7f8365d 100644 --- a/baml_language/crates/baml_compiler2_ast/src/ast.rs +++ b/baml_language/crates/baml_compiler2_ast/src/ast.rs @@ -1623,7 +1623,6 @@ pub enum BuiltinKind { #[derive(Debug, Clone, PartialEq, Eq)] pub struct LlmBodyDef { pub client: Option, - pub prompt: Option, /// Pre-lowered companion bodies keyed by target name. The single-path /// world stashes exactly one: `"spec"` — the `$spec` body, built in /// `lower_cst` while the CST backtick is still in hand (the AST must stay @@ -1645,15 +1644,6 @@ pub struct LlmBodyDef { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RawPrompt { pub text: std::string::String, - /// Interpolation locations within the template. - pub interpolations: Vec, - pub span: TextRange, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Interpolation { - pub content: std::string::String, - /// Span of the full interpolation, including delimiters. pub span: TextRange, } diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs index 95c9ab5d8fa..047f8fb4f41 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs @@ -17,8 +17,8 @@ use crate::{ ast::{ AssociatedTypeBindingDef, AssociatedTypeDef, BuiltinKind, CallArg, EnumDef, Expr, ExprId, FieldDef, FunctionBodyDef, FunctionDef, FunctionDefaults, ImplementsBlockDef, - ImplementsForDef, InterfaceDef, InterfaceFieldLinkDef, Interpolation, Item, LambdaDef, - LambdaKind, LlmBodyDef, MethodSigDef, Param, RawAttribute, RawAttributeArg, RawPrompt, + ImplementsForDef, InterfaceDef, InterfaceFieldLinkDef, Item, LambdaDef, LambdaKind, + LlmBodyDef, MethodSigDef, Param, RawAttribute, RawAttributeArg, RawPrompt, TemplateStringDef, TestArgValue, TestDef, TypeAliasDef, TypeExpr, TypeExprKind, VariantDef, }, companions::expand_companions, @@ -343,10 +343,10 @@ fn lower_function( // Jinja prompts are removed: the single-path world renders prompts as // plain backtick templates through the spec. - if llm.prompt_field().and_then(|pf| pf.raw_string()).is_some() { + if let Some(raw_prompt) = llm.prompt_field().and_then(|pf| pf.raw_string()) { diags.push(LoweringDiagnostic::LlmJinjaPromptRemoved { function_name: name.as_str().to_string(), - span: llm_body_def.span, + span: raw_prompt.syntax().span_range(), }); } @@ -693,14 +693,8 @@ fn lower_llm_body(llm_body: &ast::LlmFunctionBody) -> LlmBodyDef { .and_then(|cf| cf.value()) .map(|name| Name::new(&name)); - let prompt = llm_body - .prompt_field() - .and_then(|pf| pf.raw_string()) - .map(|raw_str| lower_raw_prompt(&raw_str)); - LlmBodyDef { client, - prompt, // Filled in by the LLM-function branch once param names are known. companion_bodies: Vec::new(), has_tools: llm_tools_present(llm_body), @@ -828,46 +822,10 @@ fn tools_value_element( } fn lower_raw_prompt(raw_string: &ast::RawStringLiteral) -> RawPrompt { - use baml_compiler_syntax::{ - SyntaxKind, - ast::{JinjaExpression, JinjaStatement, PromptText}, - }; - - let mut text = String::new(); - let mut interpolations = Vec::new(); let prompt_span = raw_string.syntax().span_range(); - - for child in raw_string.syntax().children() { - match child.kind() { - SyntaxKind::PROMPT_TEXT => { - if let Some(prompt_text) = PromptText::cast(child.clone()) { - text.push_str(&prompt_text.text()); - } - } - SyntaxKind::TEMPLATE_INTERPOLATION => { - if let Some(jinja_expr) = JinjaExpression::cast(child.clone()) { - let inner = jinja_expr.inner_text(); - let full = jinja_expr.full_text(); - let span = child.span_range(); - interpolations.push(Interpolation { - content: inner, - span, - }); - text.push_str(&full); - } - } - SyntaxKind::TEMPLATE_CONTROL => { - if let Some(jinja_stmt) = JinjaStatement::cast(child.clone()) { - text.push_str(&jinja_stmt.full_text()); - } - } - _ => {} - } - } - RawPrompt { - text, - interpolations, + text: crate::parse_string_attr_value(&raw_string.syntax().text().to_string()) + .unwrap_or_default(), span: prompt_span, } } diff --git a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs index 0eefb702d16..6dce7b74280 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs @@ -585,11 +585,11 @@ impl LoweringDiagnostic { DiagnosticId::InvalidSyntax, Severity::Error, format!( - "LLM function `{function_name}` uses a Jinja `#\"...\"#` prompt, which is \ - removed; write a backtick template: prompt `... ${{ctx.output_format}}`" + "LLM function `{function_name}` uses a Jinja `#\"...\"#` prompt, which is no \ + longer supported. Use a backtick prompt with `${{...}}` interpolation instead." ), *span, - "migrate to a backtick prompt", + "replace `#\"Hello {{ name }}\"#` with `` `Hello ${name}` ``", ), LoweringDiagnostic::InvalidLlmClient { function_name, diff --git a/baml_language/crates/baml_compiler2_emit/src/lib.rs b/baml_language/crates/baml_compiler2_emit/src/lib.rs index 25be35a95d6..539424fe77b 100644 --- a/baml_language/crates/baml_compiler2_emit/src/lib.rs +++ b/baml_language/crates/baml_compiler2_emit/src/lib.rs @@ -30,9 +30,8 @@ use baml_compiler2_ppir::{ function_body, item_data::{ GenericParamData, class_data, enum_data, file_classes, file_enums, file_free_impls, - file_functions, file_interfaces, file_lets, file_template_strings, file_tests, - function_data, function_llm_meta, function_llm_prompt, impl_block_data, interface_data, - method_interface_target, template_string_data, test_data, + file_functions, file_interfaces, file_lets, file_tests, function_data, function_llm_meta, + impl_block_data, interface_data, method_interface_target, test_data, }, }; use baml_type::{ParamTy, RuntimeTy, TyAttr}; @@ -1984,15 +1983,6 @@ pub fn decompose_units( units[carrier].package_fragment = frag; } - // ---- Template-string macros (Pass 5 fragment, per file) ----------------- - for (fi, file) in all_files.iter().enumerate() { - for &ts_loc in file_template_strings(db, *file) { - if let Some(macro_def) = render_template_macro(template_string_data(db, ts_loc)) { - units[fi].template_macros.push(macro_def); - } - } - } - // ---- Test cases (Pass 8 fragment, per file by source path) -------------- if options.emit_test_cases { for test in &program.test_cases { @@ -2040,24 +2030,6 @@ fn local_ref_sort_key(r: LocalRef) -> (u8, u32) { } } -/// Render one template-string definition as a Jinja `{% macro %}` block, or -/// `None` for a body-less (declaration-only) template string. -fn render_template_macro( - ts: &baml_compiler2_ppir::item_data::TemplateStringData, -) -> Option { - let body = ts.body.as_ref()?; - let args = ts - .params - .iter() - .map(|param| param.name.to_string()) - .collect::>() - .join(", "); - Some(format!( - "{{% macro {name}({args}) %}}{body}{{% endmacro %}}", - name = ts.name, - )) -} - /// Fully-qualified name of a class/enum/interface definition object. fn def_object_fq(obj: &Object) -> String { match obj { @@ -2610,17 +2582,6 @@ fn generate_impl( skip_clean, )?; - // --- Pass 5: Template string macros --- - let mut template_macros = Vec::new(); - for file in &all_files { - for &ts_loc in file_template_strings(db, *file) { - if let Some(macro_def) = render_template_macro(template_string_data(db, ts_loc)) { - template_macros.push(macro_def); - } - } - } - program.template_strings_macros = template_macros.join("\n"); - // --- Pass 6: Retry policies --- // Retry policies are now synthesized as Item::Let bindings during CST lowering. // Their values flow through the $init pipeline instead of being parsed here. @@ -5106,19 +5067,7 @@ fn attach_function_metadata<'db>( if let Some(llm_meta) = function_llm_meta(db, func_loc) && let Some(client) = &llm_meta.client_name { - // New-mode (BEP-049 M5) functions have no Jinja `prompt` text — the compiled - // closure renders it — but they still need a registry entry so - // `get_return_type` / `get_stream_return_type` (used by `__make_stream` and - // the streaming `Context`) resolve by name. The empty template is never read - // for them (their `prompt_closure` is non-null, so the Jinja - // `get_jinja_template` branch is skipped). The prompt text is fetched from - // the span-carrying `function_llm_prompt` sibling of `function_llm_meta`. - let prompt_template = function_llm_prompt(db, func_loc) - .as_ref() - .map(|p| p.text.clone()) - .unwrap_or_default(); compiled_fn.body_meta = Some(FunctionMeta::Llm { - prompt_template, client: client.to_string(), }); compiled_fn.capture = FunctionCaptureProps::disabled() @@ -5581,7 +5530,6 @@ mod tests { #[test] fn parse_raw_string_single_hash() { - // Input: #"raw\ntext"# — raw strings don't unescape let input = "#\"raw\\ntext\"#"; assert_eq!( parse_string_attr_value(input), @@ -5591,7 +5539,6 @@ mod tests { #[test] fn parse_raw_string_double_hash() { - // Input: ##"has "# inside"## let input = "##\"has \"# inside\"##"; assert_eq!( parse_string_attr_value(input), @@ -5601,7 +5548,6 @@ mod tests { #[test] fn parse_empty_raw_string() { - // Input: #""# let input = "#\"\"#"; assert_eq!(parse_string_attr_value(input), Some(String::new())); } @@ -5842,7 +5788,6 @@ mod tests { #[test] fn extract_raw_string_attr() { - // Simulates @description(#"raw desc"#) let attrs = vec![mk_attr("description", &["#\"raw desc\"#"])]; let (desc, _, _) = extract_schema_attrs(&attrs); assert_eq!(desc, Some("raw desc".to_string())); diff --git a/baml_language/crates/baml_compiler2_hir/src/item_tree/template_strings.rs b/baml_language/crates/baml_compiler2_hir/src/item_tree/template_strings.rs index ff97910176a..05c4bcbfe5d 100644 --- a/baml_language/crates/baml_compiler2_hir/src/item_tree/template_strings.rs +++ b/baml_language/crates/baml_compiler2_hir/src/item_tree/template_strings.rs @@ -8,7 +8,7 @@ pub struct TemplateString { pub name: Name, /// Template parameters with optional type annotations and spans. pub params: Vec, - /// Template body text (Jinja template). + /// Legacy template body text retained for parser recovery. pub body: Option, /// Full source span of the template string declaration. pub span: TextRange, diff --git a/baml_language/crates/baml_compiler2_ppir/src/item_data/functions.rs b/baml_language/crates/baml_compiler2_ppir/src/item_data/functions.rs index 09659b37fa1..6930a366b96 100644 --- a/baml_language/crates/baml_compiler2_ppir/src/item_data/functions.rs +++ b/baml_language/crates/baml_compiler2_ppir/src/item_data/functions.rs @@ -97,43 +97,6 @@ pub fn function_llm_meta<'db>( }) } -/// The span-carrying Jinja prompt of an LLM (`{ client …; prompt … }`) function, -/// for prompt-template validation. -/// -/// This is the body-ish sibling of [`function_llm_meta`]: because its value -/// carries the prompt's source span, it re-runs whenever the prompt text *or its -/// position* changes — it does not offer the span-free early cutoff -/// `function_llm_meta` does. It mirrors [`function_body`](crate::function_body)'s -/// span-carrying tracked shape and exists so prompt validation can front the item -/// tree without reading it directly. -/// -/// `None` for a non-LLM function or an LLM function without a `prompt`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LlmPromptBody { - pub text: String, - pub span: TextRange, -} - -/// The [`LlmPromptBody`] for one function, or `None` when it has no LLM prompt. -#[salsa::tracked] -pub fn function_llm_prompt<'db>( - db: &'db dyn crate::Db, - function: FunctionLoc<'db>, -) -> Option> { - let item_tree = crate::file_item_tree(db, function.file(db)); - item_tree[function.id(db)] - .declarative_meta - .as_ref() - .and_then(|ast::DeclarativeMeta::Llm(llm)| { - llm.prompt.as_ref().map(|prompt| { - std::sync::Arc::new(LlmPromptBody { - text: prompt.text.clone(), - span: prompt.span, - }) - }) - }) -} - /// Span-free semantic data for a function's *elaborated* signature — the /// canonical callable view TIR consumes. /// diff --git a/baml_language/crates/baml_compiler2_ppir/src/item_data/template_strings.rs b/baml_language/crates/baml_compiler2_ppir/src/item_data/template_strings.rs index 2ba3b1b9d0d..db44790e0c2 100644 --- a/baml_language/crates/baml_compiler2_ppir/src/item_data/template_strings.rs +++ b/baml_language/crates/baml_compiler2_ppir/src/item_data/template_strings.rs @@ -14,7 +14,7 @@ pub struct TemplateStringData { /// Type references in this template's parameter list. Scoped to the item. pub type_refs: TypeRefStore, pub params: Vec, - /// Template body text (Jinja). + /// Legacy template body text retained for parser recovery. pub body: Option, } diff --git a/baml_language/crates/baml_compiler_diagnostics/src/diagnostic.rs b/baml_language/crates/baml_compiler_diagnostics/src/diagnostic.rs index 0547ab040e2..22c3462ed29 100644 --- a/baml_language/crates/baml_compiler_diagnostics/src/diagnostic.rs +++ b/baml_language/crates/baml_compiler_diagnostics/src/diagnostic.rs @@ -170,26 +170,6 @@ pub enum DiagnosticId { AliasCycle, ClassCycle, - // Jinja template diagnostics (E0070-E0086) - JinjaUnresolvedVariable, - JinjaFunctionReferenceWithoutCall, - JinjaInvalidFilter, - JinjaInvalidType, - JinjaPropertyNotDefined, - JinjaEnumValuePropertyAccess, - JinjaEnumStringComparison, - JinjaPropertyNotFoundInUnion, - JinjaPropertyTypeMismatchInUnion, - JinjaNonClassInUnion, - JinjaWrongArgCount, - JinjaMissingArg, - JinjaUnknownArg, - JinjaWrongArgType, - JinjaParseError, - JinjaUnsupportedFeature, - JinjaInvalidSyntax, - JinjaInvalidTest, - // Catch binding errors (E0093) InvalidCatchBindingType, @@ -477,26 +457,6 @@ impl DiagnosticId { DiagnosticId::AliasCycle => "E0068", DiagnosticId::ClassCycle => "E0069", - // Jinja template diagnostics - DiagnosticId::JinjaUnresolvedVariable => "E0070", - DiagnosticId::JinjaFunctionReferenceWithoutCall => "E0071", - DiagnosticId::JinjaInvalidFilter => "E0072", - DiagnosticId::JinjaInvalidType => "E0073", - DiagnosticId::JinjaPropertyNotDefined => "E0074", - DiagnosticId::JinjaEnumValuePropertyAccess => "E0075", - DiagnosticId::JinjaEnumStringComparison => "E0076", - DiagnosticId::JinjaPropertyNotFoundInUnion => "E0077", - DiagnosticId::JinjaPropertyTypeMismatchInUnion => "E0078", - DiagnosticId::JinjaNonClassInUnion => "E0079", - DiagnosticId::JinjaWrongArgCount => "E0080", - DiagnosticId::JinjaMissingArg => "E0081", - DiagnosticId::JinjaUnknownArg => "E0082", - DiagnosticId::JinjaWrongArgType => "E0083", - DiagnosticId::JinjaParseError => "E0084", - DiagnosticId::JinjaUnsupportedFeature => "E0085", - DiagnosticId::JinjaInvalidSyntax => "E0086", - DiagnosticId::JinjaInvalidTest => "E0087", - // Reserved prefix errors DiagnosticId::ReservedStreamPrefix => "E0100", diff --git a/baml_language/crates/baml_compiler_diagnostics/src/errors/type_error.rs b/baml_language/crates/baml_compiler_diagnostics/src/errors/type_error.rs index 4cf461edf61..241960c2049 100644 --- a/baml_language/crates/baml_compiler_diagnostics/src/errors/type_error.rs +++ b/baml_language/crates/baml_compiler_diagnostics/src/errors/type_error.rs @@ -126,118 +126,6 @@ pub enum TypeError { location: C::Location, }, - // ============ Jinja Template Errors ============ - /// Variable referenced in Jinja template does not exist. - JinjaUnresolvedVariable { - name: String, - suggestions: Vec, - location: C::Location, - }, - /// Function referenced without calling it (missing parentheses). - JinjaFunctionReferenceWithoutCall { - function_name: String, - location: C::Location, - }, - /// Unknown Jinja filter. - JinjaInvalidFilter { - filter_name: String, - suggestions: Vec, - location: C::Location, - }, - /// Type mismatch in Jinja expression. - JinjaInvalidType { - expression: String, - expected: String, - found: String, - location: C::Location, - }, - /// Property access on class that doesn't have the property. - JinjaPropertyNotDefined { - variable: String, - class_name: String, - property: String, - location: C::Location, - }, - /// Property access on enum value (not allowed). - JinjaEnumValuePropertyAccess { - variable: String, - enum_value: String, - property: String, - location: C::Location, - }, - /// Comparing enum to string (deprecated). - JinjaEnumStringComparison { - enum_name: String, - location: C::Location, - }, - /// Property access on union where some members don't have the property. - JinjaPropertyNotFoundInUnion { - property: String, - missing_on: Vec, - location: C::Location, - }, - /// Property has inconsistent types across union members. - JinjaPropertyTypeMismatchInUnion { - property: String, - location: C::Location, - }, - /// Union contains non-class type when accessing property. - JinjaNonClassInUnion { - variable: String, - property: String, - non_class_type: String, - location: C::Location, - }, - /// Wrong number of arguments in Jinja function call. - JinjaWrongArgCount { - function_name: String, - expected: usize, - found: usize, - location: C::Location, - }, - /// Missing required argument in Jinja function call. - JinjaMissingArg { - function_name: String, - arg_name: String, - location: C::Location, - }, - /// Unknown argument name in Jinja function call. - JinjaUnknownArg { - function_name: String, - arg_name: String, - suggestions: Vec, - location: C::Location, - }, - /// Wrong argument type in Jinja function call. - JinjaWrongArgType { - function_name: String, - arg_name: String, - expected: String, - found: String, - location: C::Location, - }, - /// Failed to parse Jinja template. - JinjaParseError { - message: String, - location: C::Location, - }, - /// Unsupported Jinja feature. - JinjaUnsupportedFeature { - feature: String, - location: C::Location, - }, - /// Invalid Jinja syntax. - JinjaInvalidSyntax { - message: String, - location: C::Location, - }, - /// Unknown Jinja test. - JinjaInvalidTest { - test_name: String, - suggestions: Vec, - location: C::Location, - }, - /// Invalid type annotation in catch binding (e.g., `any` or `unknown`). InvalidCatchBindingType { type_name: String, diff --git a/baml_language/crates/baml_compiler_diagnostics/src/to_diagnostic.rs b/baml_language/crates/baml_compiler_diagnostics/src/to_diagnostic.rs index 68657606ead..b4e9e54d2f0 100644 --- a/baml_language/crates/baml_compiler_diagnostics/src/to_diagnostic.rs +++ b/baml_language/crates/baml_compiler_diagnostics/src/to_diagnostic.rs @@ -281,250 +281,6 @@ impl TypeError { ) .with_primary_span(loc_fn(location)), - TypeError::JinjaUnresolvedVariable { - name, - suggestions, - location, - } => { - let message = if suggestions.is_empty() { - format!("variable `{name}` does not exist.") - } else if suggestions.len() == 1 { - format!( - "variable `{name}` does not exist. Did you mean `{}`?", - suggestions[0] - ) - } else { - format!( - "variable `{name}` does not exist. Did you mean one of these: `{}`?", - suggestions.join("`, `") - ) - }; - Diagnostic::warning(DiagnosticId::JinjaUnresolvedVariable, message) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaFunctionReferenceWithoutCall { - function_name, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaFunctionReferenceWithoutCall, - format!( - "function '{function_name}' referenced without parentheses. Did you mean '{function_name}()'?" - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaInvalidFilter { - filter_name, - suggestions, - location, - } => { - let message = if suggestions.is_empty() { - format!("filter '{filter_name}' does not exist") - } else if suggestions.len() == 1 { - format!( - "filter '{filter_name}' does not exist. Did you mean '{}'?", - suggestions[0] - ) - } else { - format!( - "filter '{filter_name}' does not exist. Did you mean one of these: '{}'?", - suggestions.join("', '") - ) - }; - Diagnostic::warning( - DiagnosticId::JinjaInvalidFilter, - format!( - "{message}\n\nSee: https://docs.rs/minijinja/latest/minijinja/filters/index.html#functions" - ), - ) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaInvalidType { - expression, - expected, - found, - location, - } => { - let found_desc = if found == "undefined" { - "undefined".to_string() - } else { - format!("a {found}") - }; - Diagnostic::warning( - DiagnosticId::JinjaInvalidType, - format!("'{expression}' is {found_desc}, expected {expected}"), - ) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaPropertyNotDefined { - variable, - class_name, - property, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaPropertyNotDefined, - format!("class {class_name} ({variable}) does not have a property '{property}'"), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaEnumValuePropertyAccess { - variable, - enum_value, - property, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaEnumValuePropertyAccess, - format!( - "enum value {enum_value} ({variable}) does not have a property '{property}'" - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaEnumStringComparison { enum_name, location } => Diagnostic::warning( - DiagnosticId::JinjaEnumStringComparison, - format!( - "comparing enum {enum_name} to string - enum-string comparisons will soon be deprecated. Please see https://github.com/BoundaryML/baml/issues/2339." - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaPropertyNotFoundInUnion { - property, - missing_on, - location, - } => { - let classes_str = missing_on.join(", "); - Diagnostic::warning( - DiagnosticId::JinjaPropertyNotFoundInUnion, - format!("property '{property}' does not exist on {classes_str}"), - ) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaPropertyTypeMismatchInUnion { property, location } => { - Diagnostic::warning( - DiagnosticId::JinjaPropertyTypeMismatchInUnion, - format!( - "property '{property}' has inconsistent types across union members" - ), - ) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaNonClassInUnion { - variable, - property, - non_class_type, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaNonClassInUnion, - format!( - "cannot access property '{property}' on '{variable}': union contains non-class type {non_class_type}" - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaWrongArgCount { - function_name, - expected, - found, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaWrongArgCount, - format!( - "function '{function_name}' expects {expected} arguments, but got {found}" - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaMissingArg { - function_name, - arg_name, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaMissingArg, - format!("function '{function_name}' expects argument '{arg_name}'"), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaUnknownArg { - function_name, - arg_name, - suggestions, - location, - } => { - let message = if suggestions.is_empty() { - format!( - "function '{function_name}' does not have an argument '{arg_name}'" - ) - } else if suggestions.len() == 1 { - format!( - "function '{function_name}' does not have an argument '{arg_name}'. Did you mean '{}'?", - suggestions[0] - ) - } else { - format!( - "function '{function_name}' does not have an argument '{arg_name}'. Did you mean one of these: '{}'?", - suggestions.join("', '") - ) - }; - Diagnostic::warning(DiagnosticId::JinjaUnknownArg, message) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaWrongArgType { - function_name, - arg_name, - expected, - found, - location, - } => Diagnostic::warning( - DiagnosticId::JinjaWrongArgType, - format!( - "function '{function_name}' expects argument '{arg_name}' to be of type {expected}, but got {found}" - ), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaParseError { message, location } => { - Diagnostic::warning( - DiagnosticId::JinjaParseError, - format!("failed to parse Jinja template: {message}"), - ) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaUnsupportedFeature { feature, location } => Diagnostic::warning( - DiagnosticId::JinjaUnsupportedFeature, - format!("{feature} are not yet supported"), - ) - .with_primary_span(loc_fn(location)), - - TypeError::JinjaInvalidSyntax { message, location } => { - Diagnostic::warning(DiagnosticId::JinjaInvalidSyntax, message.clone()) - .with_primary_span(loc_fn(location)) - } - - TypeError::JinjaInvalidTest { - test_name, - suggestions, - location, - } => { - let msg = if suggestions.is_empty() { - format!("unknown test '{test_name}'") - } else { - format!( - "unknown test '{test_name}'. Valid tests: {}", - suggestions.join(", ") - ) - }; - Diagnostic::warning(DiagnosticId::JinjaInvalidTest, msg) - .with_primary_span(loc_fn(location)) - } - TypeError::InvalidCatchBindingType { type_name, location, diff --git a/baml_language/crates/baml_compiler_lexer/src/tokens.rs b/baml_language/crates/baml_compiler_lexer/src/tokens.rs index 7770cc1ac52..ddc1c439e4f 100644 --- a/baml_language/crates/baml_compiler_lexer/src/tokens.rs +++ b/baml_language/crates/baml_compiler_lexer/src/tokens.rs @@ -754,7 +754,7 @@ mod tests { } #[test] - fn test_raw_string_with_jinja() { + fn test_raw_string_with_braces() { let source = r##"#"Hello {{ name }}"#"##; let tokens = lex_no_whitespace(source); diff --git a/baml_language/crates/baml_compiler_parser/src/parser.rs b/baml_language/crates/baml_compiler_parser/src/parser.rs index 1d374b84ef2..e20e7a6c741 100644 --- a/baml_language/crates/baml_compiler_parser/src/parser.rs +++ b/baml_language/crates/baml_compiler_parser/src/parser.rs @@ -1982,14 +1982,14 @@ impl<'a> Parser<'a> { } p.bump(); // Opening " - // Parse raw string content with Jinja template support + // Parse raw string content as literal text. p.parse_raw_string_content(opening_hashes); }); true } - /// Parse the content inside a raw string, recognizing Jinja template constructs + /// Parse the content inside a raw string. fn parse_raw_string_content(&mut self, opening_hashes: usize) { let mut loop_counter = 0; @@ -2023,159 +2023,10 @@ impl<'a> Parser<'a> { } } - // Check for Jinja constructs - if self.at_jinja_expression() { - self.parse_jinja_expression(opening_hashes); - } else if self.at_jinja_statement() { - self.parse_jinja_statement(opening_hashes); - } else if self.at_jinja_comment() { - self.parse_jinja_comment(opening_hashes); - } else { - // Plain text content - collect tokens until we hit a Jinja construct or closing delimiter - self.parse_prompt_text(opening_hashes); - } + self.bump_raw(); } } - /// Check if we're at the start of a Jinja expression: {{ - fn at_jinja_expression(&self) -> bool { - self.at_raw(TokenKind::LBrace) - && self.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::LBrace) - } - - /// Check if we're at the start of a Jinja statement: {% - fn at_jinja_statement(&self) -> bool { - self.at_raw(TokenKind::LBrace) - && self.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::Percent) - } - - /// Check if we're at the start of a Jinja comment: {# - fn at_jinja_comment(&self) -> bool { - self.at_raw(TokenKind::LBrace) - && self.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::Hash) - } - - /// Parse a Jinja expression: {{ ... }} - fn parse_jinja_expression(&mut self, opening_hashes: usize) { - self.with_node(SyntaxKind::TEMPLATE_INTERPOLATION, |p| { - p.bump_raw(); // { - p.bump_raw(); // { - - // Collect tokens until we find }} - let mut depth = 1; - while !p.at_end_raw() && depth > 0 { - if p.at_raw(TokenKind::Quote) - && p.count_consecutive_hashes_after_quote() == opening_hashes - { - p.error_unexpected_token("Unclosed Jinja expression (expected }})".to_string()); - return; - } - if p.at_raw(TokenKind::LBrace) - && p.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::LBrace) - { - depth += 1; - p.bump_raw(); - p.bump_raw(); - } else if p.at_raw(TokenKind::RBrace) - && p.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::RBrace) - { - depth -= 1; - if depth == 0 { - p.bump_raw(); // } - p.bump_raw(); // } - break; - } - p.bump_raw(); - p.bump_raw(); - } else { - p.bump_raw(); - } - } - - if depth > 0 { - p.error_unexpected_token("Unclosed Jinja expression (expected }})".to_string()); - } - }); - } - - /// Parse a Jinja statement: {% ... %} - fn parse_jinja_statement(&mut self, opening_hashes: usize) { - self.with_node(SyntaxKind::TEMPLATE_CONTROL, |p| { - p.bump_raw(); // { - p.bump_raw(); // % - - // Collect tokens until we find %} - while !p.at_end_raw() { - if p.at_raw(TokenKind::Quote) - && p.count_consecutive_hashes_after_quote() == opening_hashes - { - p.error_unexpected_token("Unclosed Jinja statement (expected %})".to_string()); - return; - } - if p.at_raw(TokenKind::Percent) - && p.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::RBrace) - { - p.bump_raw(); // % - p.bump_raw(); // } - break; - } - p.bump_raw(); - } - }); - } - - /// Parse a Jinja comment: {# ... #} - fn parse_jinja_comment(&mut self, opening_hashes: usize) { - self.with_node(SyntaxKind::TEMPLATE_COMMENT, |p| { - p.bump_raw(); // { - p.bump_raw(); // # - - // Collect tokens until we find #} - while !p.at_end_raw() { - if p.at_raw(TokenKind::Quote) - && p.count_consecutive_hashes_after_quote() == opening_hashes - { - p.error_unexpected_token("Unclosed Jinja comment (expected #})".to_string()); - return; - } - if p.at_raw(TokenKind::Hash) - && p.peek_impl(1, false).map(|t| t.kind) == Some(TokenKind::RBrace) - { - p.bump_raw(); // # - p.bump_raw(); // } - break; - } - p.bump_raw(); - } - }); - } - - /// Parse plain text content between Jinja constructs - /// - /// Will consume trailing whitespace as well. - fn parse_prompt_text(&mut self, opening_hashes: usize) { - self.with_node(SyntaxKind::PROMPT_TEXT, |p| { - // Collect tokens until we hit a Jinja construct or closing delimiter - while !p.at_end_raw() { - // Check for closing delimiter - if p.at_raw(TokenKind::Quote) { - let closing_hashes = p.count_consecutive_hashes_after_quote(); - if closing_hashes == opening_hashes { - break; - } - } - - // Check for Jinja constructs - if p.at_jinja_expression() || p.at_jinja_statement() || p.at_jinja_comment() { - while p.eat_basic_trivia() {} // make it part of the PROMPT_TEXT - break; - } - - p.bump_raw(); - } - }); - } - /// Parse a string or raw string (dispatches to correct method) pub(crate) fn parse_any_string(&mut self) -> bool { if self.at(TokenKind::Hash) { @@ -8166,7 +8017,13 @@ impl<'a> Parser<'a> { /// Parse a template string declaration pub(crate) fn parse_template_string(&mut self) { self.with_node(SyntaxKind::TEMPLATE_STRING_DEF, |p| { - // 'template_string' keyword + p.error_here( + "`template_string` is no longer supported. Use a function returning a backtick string instead." + .to_string(), + ); + + // Keep parsing the declaration for lossless recovery and to avoid + // cascading diagnostics after the unsupported keyword. p.expect(TokenKind::TemplateString); // Template name @@ -9277,8 +9134,8 @@ client Foo { } #[test] - fn raw_string_keeps_comment_markers_as_text() { - for marker in ["//", "*/"] { + fn raw_string_keeps_template_markers_as_text() { + for marker in ["//", "*/", "{{ name }}", "{% if true %}", "{# note #}"] { let source = format!( r##" function Demo() -> string {{ @@ -9301,6 +9158,28 @@ function Demo() -> string {{ } } + #[test] + fn template_string_reports_targeted_unsupported_error() { + let source = "template_string Greeting(name: string) `Hello ${name}`"; + let (root, errors) = parse_source(source); + + assert_eq!(errors.len(), 1); + assert!( + matches!( + &errors[0], + ParseError::InvalidSyntax { message, .. } + if message.contains("`template_string` is no longer supported") + && message.contains("function returning a backtick string") + ), + "unexpected diagnostic: {errors:#?}" + ); + assert!( + root.descendants() + .any(|node| node.kind() == SyntaxKind::TEMPLATE_STRING_DEF), + "unsupported declaration should remain in the tree for recovery" + ); + } + #[test] fn parses_function_with_client_as_parameter_name() { // `client` is a keyword (KW_CLIENT); it must still be valid as a parameter name. diff --git a/baml_language/crates/baml_compiler_syntax/src/ast.rs b/baml_language/crates/baml_compiler_syntax/src/ast.rs index 55b7fc45cc4..2f54e2cf30d 100644 --- a/baml_language/crates/baml_compiler_syntax/src/ast.rs +++ b/baml_language/crates/baml_compiler_syntax/src/ast.rs @@ -163,12 +163,6 @@ ast_node!(BacktickStringLiteral, BACKTICK_STRING_LITERAL); ast_node!(BacktickText, BACKTICK_TEXT); ast_node!(BacktickInterpolation, BACKTICK_INTERPOLATION); -// Jinja template components (inside raw strings) -ast_node!(JinjaExpression, TEMPLATE_INTERPOLATION); -ast_node!(JinjaStatement, TEMPLATE_CONTROL); -ast_node!(JinjaComment, TEMPLATE_COMMENT); -ast_node!(PromptText, PROMPT_TEXT); - ast_node!(TypeExpr, TYPE_EXPR); ast_node!(Attribute, ATTRIBUTE); ast_node!(TypeBuilderBlock, TYPE_BUILDER_BLOCK); @@ -1152,19 +1146,15 @@ impl ClientField { } impl PromptField { - /// Get the raw string literal node containing the prompt. - /// - /// For `prompt #"Hello {{ name }}"#`, returns the `#"Hello {{ name }}"#` node - /// (the legacy Jinja form). Returns `None` for a new-mode backtick prompt. + /// Get a legacy raw-string prompt for migration diagnostics. pub fn raw_string(&self) -> Option { self.syntax.children().find_map(RawStringLiteral::cast) } - /// Get the backtick string literal node containing a new-mode prompt. + /// Get the backtick string literal node containing the prompt. /// /// For `` prompt `Hello ${name}` ``, returns the `` `Hello ${name}` `` node. - /// BEP-049 (M5f): a backtick prompt compiles to a prompt-tag closure instead - /// of a stored Jinja template. Returns `None` for a `#"..."#` prompt. + /// A backtick prompt compiles to a prompt-tag closure. pub fn backtick_string(&self) -> Option { self.syntax.children().find_map(BacktickStringLiteral::cast) } @@ -1775,40 +1765,6 @@ pub struct BacktickIfBranch { pub body: Vec, } -impl JinjaExpression { - /// Get the inner text of the Jinja expression, without the {{ }} delimiters. - /// - /// For `{{ input.name }}`, returns `input.name` (with whitespace trimmed). - pub fn inner_text(&self) -> String { - let text = self.syntax.text().to_string(); - // Strip {{ and }} - if text.starts_with("{{") && text.ends_with("}}") { - text[2..text.len() - 2].trim().to_string() - } else { - text - } - } - - /// Get the full text of the Jinja expression, including {{ }} delimiters. - pub fn full_text(&self) -> String { - self.syntax.text().to_string() - } -} - -impl JinjaStatement { - /// Get the full text of the Jinja statement, including {% %} delimiters. - pub fn full_text(&self) -> String { - self.syntax.text().to_string() - } -} - -impl PromptText { - /// Get the text content. - pub fn text(&self) -> String { - self.syntax.text().to_string() - } -} - impl Parameter { /// Get the parameter name. pub fn name(&self) -> Option { diff --git a/baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs b/baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs index 209bbfe582b..c4f30af058f 100644 --- a/baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs +++ b/baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs @@ -443,13 +443,6 @@ pub enum SyntaxKind { BACKTICK_ELSE, // ${else} BACKTICK_ENDIF, // ${endif} - // Template components (inside raw strings) - TEMPLATE_CONTENT, // Plain text (deprecated, use PROMPT_TEXT) - TEMPLATE_INTERPOLATION, // {{ expr }} - Jinja expressions - TEMPLATE_CONTROL, // {% for ... %} - Jinja statements - TEMPLATE_COMMENT, // {# comment #} - Jinja comments - PROMPT_TEXT, // Plain text between Jinja constructs - // Error recovery ERROR, diff --git a/baml_language/crates/baml_fmt/src/lib.rs b/baml_language/crates/baml_fmt/src/lib.rs index 7946668ece8..9161df4d71c 100644 --- a/baml_language/crates/baml_fmt/src/lib.rs +++ b/baml_language/crates/baml_fmt/src/lib.rs @@ -648,6 +648,17 @@ mod backtick_format_tests { assert_eq!(formatted, second); } + #[test] + fn backtick_function_return_dedents() { + let source = "function Foo(name: string) -> string {\n `\n Hello ${name}\n Bye\n `\n}\n"; + let expected = "function Foo(name: string) -> string {\n `\n Hello ${name}\n Bye\n `\n}\n"; + let options = FormatOptions::default(); + let formatted = format(source, &options).expect("formatter should succeed"); + assert_eq!(formatted, expected, "got:\n{formatted}"); + let second = format(&formatted, &options).expect("formatter should be idempotent"); + assert_eq!(formatted, second); + } + /// A single-line backtick prompt is accepted and printed verbatim. #[test] fn backtick_prompt_one_liner_accepted() { @@ -673,19 +684,6 @@ mod backtick_format_tests { assert_eq!(formatted, second); } - /// A backtick `template_string` body is accepted and its interior re-indented - /// (closing backtick at column 0, like a raw-string body). - #[test] - fn backtick_template_string_dedents() { - let source = "template_string Foo(name: string) `\n Hello ${name}\n Bye\n`\n"; - let expected = "template_string Foo(name: string) `\n Hello ${name}\n Bye\n`\n"; - let options = FormatOptions::default(); - let formatted = format(source, &options).expect("formatter should succeed"); - assert_eq!(formatted, expected, "got:\n{formatted}"); - let second = format(&formatted, &options).expect("formatter should be idempotent"); - assert_eq!(formatted, second); - } - /// Backtick string in expression position re-indents its over-indented /// interior to the surrounding block. #[test] diff --git a/baml_language/crates/baml_lsp2_actions/Cargo.toml b/baml_language/crates/baml_lsp2_actions/Cargo.toml index 4bda7c004d0..1b49621b767 100644 --- a/baml_language/crates/baml_lsp2_actions/Cargo.toml +++ b/baml_language/crates/baml_lsp2_actions/Cargo.toml @@ -16,7 +16,6 @@ baml_compiler2_tir = { workspace = true } baml_compiler_diagnostics = { workspace = true } baml_compiler_parser = { workspace = true } baml_compiler_syntax = { workspace = true } -sys_jinja_types = { workspace = true } bitflags = { workspace = true } indexmap = { workspace = true } rowan = { workspace = true } diff --git a/baml_language/crates/baml_lsp2_actions/src/actions.rs b/baml_language/crates/baml_lsp2_actions/src/actions.rs index 081a4a8bfec..3d2d13b25bb 100644 --- a/baml_language/crates/baml_lsp2_actions/src/actions.rs +++ b/baml_language/crates/baml_lsp2_actions/src/actions.rs @@ -187,7 +187,7 @@ mod tests { r##" function Summarize(input: string) -> string { client GPT4 - prompt #"Summarize {{ input }}"# + prompt `Summarize ${input}` } "##, ); diff --git a/baml_language/crates/baml_lsp2_actions/src/annotations.rs b/baml_language/crates/baml_lsp2_actions/src/annotations.rs index e174f329516..ff92134023e 100644 --- a/baml_language/crates/baml_lsp2_actions/src/annotations.rs +++ b/baml_language/crates/baml_lsp2_actions/src/annotations.rs @@ -392,7 +392,7 @@ mod tests { r##" function Summarize(input: string) -> string { client GPT4 - prompt #"Summarize {{ input }}"# + prompt `Summarize ${input}` } function Echo(x: string) -> string { diff --git a/baml_language/crates/baml_lsp2_actions/src/check.rs b/baml_language/crates/baml_lsp2_actions/src/check.rs index 70db918952d..cde592ceca0 100644 --- a/baml_language/crates/baml_lsp2_actions/src/check.rs +++ b/baml_language/crates/baml_lsp2_actions/src/check.rs @@ -32,8 +32,7 @@ use baml_compiler2_tir::{ inference::render_scope_diagnostics, ty::{ParamTy, Ty, TyAttr}, }; -use indexmap::IndexMap; -use text_size::{TextRange, TextSize}; +use text_size::TextRange; use crate::Db; @@ -178,21 +177,7 @@ pub fn check_file(db: &dyn Db, file: SourceFile) -> Vec { &pkg_info.namespace_path, )); - // ── 5. Jinja prompt/template diagnostics ──────────────────────────────── - // - // Declarative LLM prompts and template_strings are MiniJinja templates, not - // regular expression bodies. Validate them with the shared MiniJinja AST - // type checker so prompt diagnostics match runtime template semantics. - let source_text = file.text(db); - diagnostics.extend(check_jinja_templates( - db, - file_id, - file, - pkg_items, - &pkg_info.package, - &pkg_info.namespace_path, - source_text, - )); + // Backtick templates are parsed and type-checked by the compiler. // ── 6. Function signature diagnostics ──────────────────────────────────── // @@ -1142,498 +1127,6 @@ fn validate_ambiguous_typevar_associated_projection_in_type_expr( } } -fn check_jinja_templates( - db: &dyn Db, - file_id: FileId, - file: SourceFile, - pkg_items: &baml_compiler2_hir::package::PackageItems<'_>, - package: &Name, - namespace_path: &[Name], - source_text: &str, -) -> Vec { - // Files with no LLM prompts and no template_string bodies have nothing to - // validate. Return before touching the package-wide Jinja environment so - // such files never pay for (or depend on) it at all. - let has_prompts = baml_compiler2_ppir::item_data::file_functions(db, file) - .iter() - .any(|&func_loc| { - baml_compiler2_ppir::item_data::function_llm_prompt(db, func_loc).is_some() - }); - let has_templates = baml_compiler2_ppir::item_data::file_template_strings(db, file) - .iter() - .any(|&template_loc| { - baml_compiler2_ppir::item_data::template_string_data(db, template_loc) - .body - .is_some() - }); - if !has_prompts && !has_templates { - return Vec::new(); - } - - let ns_id = baml_compiler2_hir::namespace::NamespaceId::new( - db, - package.clone(), - namespace_path.to_vec(), - ); - let base_types = jinja_base_types(db, ns_id); - let mut diagnostics = Vec::new(); - - for &func_loc in baml_compiler2_ppir::item_data::file_functions(db, file) { - diagnostics.extend(check_llm_prompt_template( - db, - file_id, - func_loc, - pkg_items, - namespace_path, - base_types, - source_text, - )); - } - - for &template_loc in baml_compiler2_ppir::item_data::file_template_strings(db, file) { - let template = baml_compiler2_ppir::item_data::template_string_data(db, template_loc); - let Some(body) = &template.body else { - continue; - }; - - let mut types = base_types.clone(); - types.start_scope(); - for param in &template.params { - let ty = param - .type_ref - .map(|id| { - jinja_type_from_type_ref(db, &template.type_refs, id, pkg_items, namespace_path) - }) - .unwrap_or(sys_jinja_types::Type::Unknown); - types.add_variable(param.name.as_str(), ty); - } - - let range_hint = - baml_compiler2_ppir::item_data::template_string_source_map(db, template_loc).span; - diagnostics.extend(render_jinja_validation_result( - file_id, - source_text, - range_hint, - body, - sys_jinja_types::validate_template(template.name.as_str(), body, &mut types), - )); - } - - diagnostics -} - -fn check_llm_prompt_template( - db: &dyn Db, - file_id: FileId, - func_loc: baml_compiler2_hir::loc::FunctionLoc<'_>, - pkg_items: &baml_compiler2_hir::package::PackageItems<'_>, - namespace_path: &[Name], - base_types: &sys_jinja_types::PredefinedTypes, - source_text: &str, -) -> Vec { - let Some(prompt) = baml_compiler2_ppir::item_data::function_llm_prompt(db, func_loc) else { - return Vec::new(); - }; - - let func_data = baml_compiler2_ppir::item_data::function_data(db, func_loc); - let mut types = base_types.clone(); - types.start_scope(); - for param in &func_data.params { - let ty = param - .type_ref - .map(|id| { - jinja_type_from_type_ref(db, &func_data.type_refs, id, pkg_items, namespace_path) - }) - .unwrap_or(sys_jinja_types::Type::Unknown); - types.add_variable(param.name.as_str(), ty); - } - - render_jinja_validation_result( - file_id, - source_text, - prompt.span, - &prompt.text, - sys_jinja_types::validate_template(func_data.name.as_str(), &prompt.text, &mut types), - ) -} - -/// Package-wide Jinja type environment (classes, enums, aliases, and -/// `template_strings` visible from a namespace), memoized per -/// (package, namespace path). -/// -/// `build_jinja_types` walks every type in the package, so before this was a -/// Salsa query it re-ran O(package items) work once per checked file — the -/// dominant quadratic term in whole-project checks (and it ran a second time -/// per file inside `get_bytecode`'s error gate). As a tracked function the -/// walk runs once per (package, namespace, revision) and every later -/// `check_file` in the same revision gets a memo hit; `PredefinedTypes`'s -/// `PartialEq` gives Salsa backdating, so edits that leave the environment -/// unchanged don't invalidate downstream prompt checks. -#[salsa::tracked(returns(ref))] -fn jinja_base_types<'db>( - db: &'db dyn Db, - ns_id: baml_compiler2_hir::namespace::NamespaceId<'db>, -) -> sys_jinja_types::PredefinedTypes { - let pkg_id = baml_compiler2_hir::package::PackageId::new(db, ns_id.package(db)); - // Match the call-site environment exactly: `check_file` resolves items via - // `package_resolution_context(...).own_items`, which is ppir's - // `package_items` — use the same query here so the env is identical. - let pkg_items = baml_compiler2_ppir::package_items(db, pkg_id); - build_jinja_types(db, pkg_items, &ns_id.path(db)) -} - -fn build_jinja_types( - db: &dyn Db, - pkg_items: &baml_compiler2_hir::package::PackageItems<'_>, - namespace_path: &[Name], -) -> sys_jinja_types::PredefinedTypes { - use baml_compiler2_hir::contributions::Definition; - - let mut types = - sys_jinja_types::PredefinedTypes::default(sys_jinja_types::JinjaContext::Prompt); - types.add_variable("baml", sys_jinja_types::Type::Unknown); - - let Some(ns_items) = pkg_items.namespaces.get(namespace_path) else { - return types; - }; - - for def in ns_items.types.values() { - let Definition::Class(class_loc) = *def else { - continue; - }; - let file = class_loc.file(db); - let class_data = baml_compiler2_ppir::item_data::class_data(db, class_loc); - let class_namespace = - baml_compiler2_hir::file_package::file_package(db, file).namespace_path; - let fields = class_data - .fields - .iter() - .map(|field| { - let ty = jinja_type_from_type_ref( - db, - &class_data.type_refs, - field.type_ref, - pkg_items, - &class_namespace, - ); - (field.name.to_string(), ty) - }) - .collect::>(); - types.add_class(class_data.name.as_str(), fields); - } - - for def in ns_items.types.values() { - let Definition::Enum(enum_loc) = *def else { - continue; - }; - let enum_data = baml_compiler2_ppir::item_data::enum_data(db, enum_loc); - types.add_enum( - enum_data.name.as_str(), - enum_data - .variants - .iter() - .map(|variant| variant.name.to_string()) - .collect(), - ); - } - - for def in ns_items.types.values() { - let Definition::TypeAlias(alias_loc) = *def else { - continue; - }; - let file = alias_loc.file(db); - let alias_data = baml_compiler2_ppir::item_data::type_alias_data(db, alias_loc); - if let Some(id) = alias_data.value { - let alias_namespace = - baml_compiler2_hir::file_package::file_package(db, file).namespace_path; - types.add_alias( - alias_data.name.as_str(), - jinja_type_from_type_ref( - db, - &alias_data.type_refs, - id, - pkg_items, - &alias_namespace, - ), - ); - } - } - - for def in ns_items.values.values() { - let Definition::TemplateString(template_loc) = *def else { - continue; - }; - let file = template_loc.file(db); - let template = baml_compiler2_ppir::item_data::template_string_data(db, template_loc); - let template_namespace = - baml_compiler2_hir::file_package::file_package(db, file).namespace_path; - let args = template - .params - .iter() - .map(|param| { - let ty = param - .type_ref - .map(|id| { - jinja_type_from_type_ref( - db, - &template.type_refs, - id, - pkg_items, - &template_namespace, - ) - }) - .unwrap_or(sys_jinja_types::Type::Unknown); - (param.name.to_string(), ty) - }) - .collect(); - types.add_function(template.name.as_str(), sys_jinja_types::Type::String, args); - } - - types -} - -fn jinja_type_from_type_ref( - db: &dyn Db, - store: &baml_compiler2_hir::type_ref::TypeRefStore, - id: baml_compiler2_hir::type_ref::TypeRefId, - pkg_items: &baml_compiler2_hir::package::PackageItems<'_>, - namespace_path: &[Name], -) -> sys_jinja_types::Type { - jinja_type_from_type_ref_inner( - db, - store, - id, - pkg_items, - namespace_path, - &mut HashSet::new(), - ) -} - -fn jinja_type_from_type_ref_inner( - db: &dyn Db, - store: &baml_compiler2_hir::type_ref::TypeRefStore, - id: baml_compiler2_hir::type_ref::TypeRefId, - pkg_items: &baml_compiler2_hir::package::PackageItems<'_>, - namespace_path: &[Name], - resolving_aliases: &mut HashSet, -) -> sys_jinja_types::Type { - use baml_compiler2_hir::{contributions::Definition, type_ref::TypeRefKind}; - use sys_jinja_types::Type; - - match &store[id].kind { - TypeRefKind::Int => Type::Int, - TypeRefKind::Bigint => Type::Bigint, - TypeRefKind::Float => Type::Float, - TypeRefKind::String => Type::String, - TypeRefKind::Bool => Type::Bool, - TypeRefKind::Null => Type::None, - TypeRefKind::Media { kind } => match kind { - baml_base::MediaKind::Image => Type::Image, - baml_base::MediaKind::Audio => Type::Audio, - _ => Type::Unknown, - }, - TypeRefKind::Literal { value } => Type::Literal(value.clone()), - TypeRefKind::Optional { inner } => Type::merge([ - Type::None, - jinja_type_from_type_ref_inner( - db, - store, - *inner, - pkg_items, - namespace_path, - resolving_aliases, - ), - ]), - TypeRefKind::List { inner } => Type::List(Box::new(jinja_type_from_type_ref_inner( - db, - store, - *inner, - pkg_items, - namespace_path, - resolving_aliases, - ))), - TypeRefKind::Map { value, .. } => Type::Map( - Box::new(Type::String), - Box::new(jinja_type_from_type_ref_inner( - db, - store, - *value, - pkg_items, - namespace_path, - resolving_aliases, - )), - ), - TypeRefKind::Union { variants } => Type::merge(variants.iter().map(|&variant| { - jinja_type_from_type_ref_inner( - db, - store, - variant, - pkg_items, - namespace_path, - resolving_aliases, - ) - })), - TypeRefKind::Path { segments, .. } if !segments.is_empty() => { - let (lookup_namespace, name) = jinja_lookup_path(namespace_path, segments); - let key = format!( - "{}::{}", - lookup_namespace - .iter() - .map(Name::as_str) - .collect::>() - .join("::"), - name - ); - let name_obj = Name::new(name.as_str()); - match pkg_items.lookup_type(&lookup_namespace, &name_obj) { - Some(Definition::Class(_)) => Type::ClassRef(name), - Some(Definition::Enum(_)) => Type::EnumTypeRef(name), - Some(Definition::TypeAlias(alias_loc)) => { - if !resolving_aliases.insert(key.clone()) { - return Type::RecursiveTypeAlias(name); - } - let file = alias_loc.file(db); - let alias = baml_compiler2_ppir::item_data::type_alias_data(db, alias_loc); - let alias_namespace = - baml_compiler2_hir::file_package::file_package(db, file).namespace_path; - let resolved = alias - .value - .map(|value_id| { - jinja_type_from_type_ref_inner( - db, - &alias.type_refs, - value_id, - pkg_items, - &alias_namespace, - resolving_aliases, - ) - }) - .unwrap_or(Type::Unknown); - resolving_aliases.remove(&key); - Type::Alias { - name, - target: Box::new(resolved.clone()), - resolved: Box::new(resolved), - } - } - _ => Type::Unknown, - } - } - TypeRefKind::Function { .. } | TypeRefKind::AssociatedTypeProjection { .. } => { - Type::Unknown - } - TypeRefKind::Uint8Array - | TypeRefKind::Never - | TypeRefKind::Void - | TypeRefKind::BuiltinUnknown - | TypeRefKind::Type - | TypeRefKind::Rust - | TypeRefKind::Error - | TypeRefKind::Unknown - | TypeRefKind::Infer - | TypeRefKind::Path { .. } => Type::Unknown, - } -} - -fn jinja_lookup_path(current_namespace: &[Name], segments: &[Name]) -> (Vec, String) { - let (name, namespace) = segments - .split_last() - .expect("caller guarantees at least one segment"); - if namespace.is_empty() { - (current_namespace.to_vec(), name.to_string()) - } else { - (namespace.to_vec(), name.to_string()) - } -} - -fn render_jinja_validation_result( - file_id: FileId, - source_text: &str, - raw_string_range: TextRange, - _template: &str, - result: Result<(), sys_jinja_types::ValidationError>, -) -> Vec { - let Err(error) = result else { - return Vec::new(); - }; - - if let Some(parse_error) = error.parsing_errors { - let range = parse_error - .range() - .map(|range| jinja_offset_range(source_text, raw_string_range, range.start, range.end)) - .unwrap_or(raw_string_range); - return vec![ - Diagnostic::error( - DiagnosticId::JinjaParseError, - format!("error parsing jinja template: {parse_error}"), - ) - .with_primary_span(Span { file_id, range }) - .with_phase(DiagnosticPhase::Type), - ]; - } - - error - .errors - .into_iter() - .map(|error| { - let span = error.span(); - let range = jinja_offset_range( - source_text, - raw_string_range, - span.start_offset as usize, - span.end_offset as usize, - ); - Diagnostic::warning(jinja_diagnostic_id(error.message()), error.message()) - .with_primary_span(Span { file_id, range }) - .with_phase(DiagnosticPhase::Type) - }) - .collect() -} - -fn raw_string_content_start(source_text: &str, raw_string_range: TextRange) -> TextSize { - let start: usize = raw_string_range.start().into(); - let end: usize = raw_string_range.end().into(); - let Some(raw_text) = source_text.get(start..end) else { - return raw_string_range.start(); - }; - let quote_offset = raw_text.find('"').unwrap_or(0); - raw_string_range.start() + TextSize::from(u32::try_from(quote_offset + 1).unwrap_or(u32::MAX)) -} - -fn jinja_offset_range( - source_text: &str, - raw_string_range: TextRange, - start_offset: usize, - end_offset: usize, -) -> TextRange { - let content_start = raw_string_content_start(source_text, raw_string_range); - TextRange::new( - content_start + TextSize::from(u32::try_from(start_offset).unwrap_or(u32::MAX)), - content_start - + TextSize::from(u32::try_from(end_offset.max(start_offset + 1)).unwrap_or(u32::MAX)), - ) -} - -fn jinja_diagnostic_id(message: &str) -> DiagnosticId { - if message.starts_with("Variable `") { - DiagnosticId::JinjaUnresolvedVariable - } else if message.contains("referenced without parentheses") { - DiagnosticId::JinjaFunctionReferenceWithoutCall - } else if message.starts_with("Filter '") { - DiagnosticId::JinjaInvalidFilter - } else if message.contains("expects argument") { - DiagnosticId::JinjaWrongArgType - } else if message.contains("expects ") && message.contains(" arguments") { - DiagnosticId::JinjaWrongArgCount - } else if message.contains("property ") { - DiagnosticId::JinjaPropertyNotDefined - } else if message.contains("enum") && message.contains("string") { - DiagnosticId::JinjaEnumStringComparison - } else { - DiagnosticId::JinjaInvalidType - } -} - fn is_function_default_signature_diagnostic( diag: &baml_compiler2_tir::infer_context::TirDiagnostic<'_>, ) -> bool { @@ -2199,197 +1692,6 @@ function demo() -> int throws never { assert!(diag.related_info.is_empty()); } - #[test] - fn check_file_reports_unknown_prompt_variable() { - let test = CursorTest::new( - r##"client GPT4o { - provider openai - options { - model "gpt-5" - api_key env.OPENAI_API_KEY - } -} - -class GuessResponse { - game_won bool - text string -} - -function TakeGuess(user_guess: string, famous_person_name: string) -> GuessResponse { - client GPT4o - prompt #" - {{ famouse_person_name | lower }} - - {{ ctx.output_format }} - "# -} -<[CURSOR]"##, - ); - - let diagnostics = check_file(&test.db, test.cursor.file); - let diag = diagnostics - .iter() - .find(|diag| diag.id == DiagnosticId::JinjaUnresolvedVariable) - .expect("unknown prompt variable diagnostic"); - - assert!(diag.message.contains("`famouse_person_name`")); - assert!(diag.message.contains("does not exist")); - let span = diag.primary_span().expect("primary span"); - let text = test.cursor.file.text(&test.db); - let start: usize = span.range.start().into(); - let end: usize = span.range.end().into(); - assert_eq!(&text[start..end], "famouse_person_name"); - } - - #[test] - fn check_file_allows_template_string_call_in_prompt() { - let test = CursorTest::new( - r##"client GPT4o { - provider openai - options { - model "gpt-5" - api_key env.OPENAI_API_KEY - } -} - -template_string GuessHeader(name: string) #" - Guess the person: {{ name }} -"# - -function TakeGuess(famous_person_name: string) -> string { - client GPT4o - prompt #" - {{ GuessHeader(famous_person_name) }} - "# -} -<[CURSOR]"##, - ); - - let diagnostics = check_file(&test.db, test.cursor.file); - assert!( - diagnostics - .iter() - .all(|diag| diag.id != DiagnosticId::JinjaUnresolvedVariable), - "template string call should not be reported as an unknown prompt variable: {diagnostics:#?}" - ); - } - - #[test] - fn check_file_reports_unknown_template_string_argument() { - let test = CursorTest::new( - r##"client GPT4o { - provider openai - options { - model "gpt-5" - api_key env.OPENAI_API_KEY - } -} - -template_string GuessHeader(name: string) #" - Guess the person: {{ name }} -"# - -function TakeGuess(famous_person_name: string) -> string { - client GPT4o - prompt #" - {{ GuessHeader(famouse_person_name) }} - "# -} -<[CURSOR]"##, - ); - - let diagnostics = check_file(&test.db, test.cursor.file); - let diag = diagnostics - .iter() - .find(|diag| diag.id == DiagnosticId::JinjaUnresolvedVariable) - .expect("unknown template string argument diagnostic"); - - assert!(diag.message.contains("`famouse_person_name`")); - assert!(diag.message.contains("does not exist")); - let span = diag.primary_span().expect("primary span"); - let text = test.cursor.file.text(&test.db); - let start: usize = span.range.start().into(); - let end: usize = span.range.end().into(); - assert_eq!(&text[start..end], "famouse_person_name"); - } - - #[test] - fn check_file_reports_template_string_call_errors() { - let test = CursorTest::new( - r##"template_string WithParams(a: int) #" - ... -"# - -template_string BadCall1() #" - {{ WithParams(a=2, b=2) }} -"# - -template_string BadCall2() #" - {{ WithParams("a") }} -"# -<[CURSOR]"##, - ); - - let diagnostics = check_file(&test.db, test.cursor.file); - assert!( - diagnostics - .iter() - .any(|diag| diag.id == DiagnosticId::JinjaWrongArgCount - && diag.message.contains("expects 1 arguments")), - "{diagnostics:#?}" - ); - assert!( - diagnostics - .iter() - .any(|diag| diag.id == DiagnosticId::JinjaWrongArgType - && diag.message.contains("expects argument 'a'")), - "{diagnostics:#?}" - ); - } - - #[test] - fn check_file_resolves_cross_file_jinja_template_strings() { - let mut builder = CursorTest::builder(); - builder.source( - "shared.baml", - r##"class Person { - name string -} - -template_string PersonHeader(person: Person) #" - {{ person.name }} -"# -"##, - ); - builder.source( - "main.baml", - r##"client GPT4o { - provider openai - options { - model "gpt-5" - api_key env.OPENAI_API_KEY - } -} - -function TakeGuess(person: Person) -> string { - client GPT4o - prompt #" - {{ PersonHeader(person) }} - "# -} -<[CURSOR]"##, - ); - let test = builder.build(); - - let diagnostics = check_file(&test.db, test.cursor.file); - assert!( - diagnostics - .iter() - .all(|diag| diag.id != DiagnosticId::JinjaUnresolvedVariable), - "cross-file template string calls should resolve in prompts: {diagnostics:#?}" - ); - } - #[test] fn check_file_reports_builtin_function_default_constraints() { let test = CursorTest::new( diff --git a/baml_language/crates/baml_lsp2_actions/src/completions.rs b/baml_language/crates/baml_lsp2_actions/src/completions.rs index e3858269dce..d0e467f759d 100644 --- a/baml_language/crates/baml_lsp2_actions/src/completions.rs +++ b/baml_language/crates/baml_lsp2_actions/src/completions.rs @@ -850,10 +850,6 @@ fn completions_for_package_path( CompletionKind::Function, format_function_signature(db, *func_loc), ), - Definition::TemplateString(_) => ( - CompletionKind::TemplateString, - "template_string".to_string(), - ), Definition::Client(_) => (CompletionKind::Client, "client".to_string()), Definition::RetryPolicy(_) => { (CompletionKind::RetryPolicy, "retry_policy".to_string()) @@ -1502,10 +1498,6 @@ fn completions_for_value_position( CompletionKind::Function, format_function_signature(db, *func_loc), ), - Definition::TemplateString(_) => ( - CompletionKind::TemplateString, - "template_string".to_string(), - ), Definition::Client(_) => (CompletionKind::Client, "client".to_string()), Definition::RetryPolicy(_) => { (CompletionKind::RetryPolicy, "retry_policy".to_string()) @@ -1584,10 +1576,6 @@ fn completions_for_top_level() -> Vec { .with_detail("retry policy declaration") .with_snippet("retry_policy ${1:Name} {\n max_retries ${2:3}\n $0\n}") .with_sort("06_retry_policy"), - Completion::new("template_string", CompletionKind::Keyword) - .with_detail("template string declaration") - .with_snippet("template_string ${1:Name}(${2}) #\"\n $0\n\"#") - .with_sort("07_template_string"), Completion::new("type", CompletionKind::Keyword) .with_detail("type alias declaration") .with_snippet("type ${1:Name} = ${2:string}$0") diff --git a/baml_language/crates/baml_lsp2_actions/src/type_info.rs b/baml_language/crates/baml_lsp2_actions/src/type_info.rs index 41edc8e79ae..962d130f5e3 100644 --- a/baml_language/crates/baml_lsp2_actions/src/type_info.rs +++ b/baml_language/crates/baml_lsp2_actions/src/type_info.rs @@ -422,12 +422,10 @@ fn literal_type_info(token: &SyntaxToken) -> Option { "An exact `float` literal type.", ), _ => { - if token.parent_ancestors().any(|node| { - matches!( - node.kind(), - SyntaxKind::TEMPLATE_INTERPOLATION | SyntaxKind::BACKTICK_INTERPOLATION - ) - }) { + if token + .parent_ancestors() + .any(|node| matches!(node.kind(), SyntaxKind::BACKTICK_INTERPOLATION)) + { return None; } let string = token.parent_ancestors().find(|node| { diff --git a/baml_language/crates/baml_lsp2_actions_tests/src/lsp_issues_authoring.rs b/baml_language/crates/baml_lsp2_actions_tests/src/lsp_issues_authoring.rs index d28fd95fb89..40b38aae9ce 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/src/lsp_issues_authoring.rs +++ b/baml_language/crates/baml_lsp2_actions_tests/src/lsp_issues_authoring.rs @@ -19,7 +19,6 @@ fn function_completion_is_a_snippet() { "client", "test", "retry_policy", - "template_string", "type", "interface", "implements", diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/completion/top_level_keywords.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/completion/top_level_keywords.baml index f025766f2ea..d0051e55212 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/completion/top_level_keywords.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/completion/top_level_keywords.baml @@ -5,4 +5,4 @@ // // //- completions -// SHOULD_CONTAIN: function, class, enum, client, test, type, retry_policy, template_string, interface, implements +// SHOULD_CONTAIN: function, class, enum, client, test, type, retry_policy, interface, implements diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string.baml index 28e2a6477bd..0e54077aa3a 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string.baml @@ -1,39 +1,33 @@ -template_string Greeting(name: string) #" - Hello, {{ name }}! -"# +function Greeting(name: string) -> string { + `Hello, ${name}!` +} -template_string NoParams() #" - A static template. -"# +function NoParams() -> string { + `A static template.` +} //---- //- diagnostics // // //- semantic_tokens -// template_string.baml:1:1 (keyword) len=15 "template_string" -// template_string.baml:1:17 (function) [declaration] len=8 "Greeting" -// template_string.baml:1:26 (parameter) [declaration] len=4 "name" -// template_string.baml:1:32 (type) [defaultLibrary] len=6 "string" -// template_string.baml:1:40 (string) len=1 "#" -// template_string.baml:1:41 (string) len=1 "\"" -// template_string.baml:2:5 (string) len=5 "Hello" -// template_string.baml:2:10 (string) len=1 "," -// template_string.baml:2:12 (string) len=1 "{" -// template_string.baml:2:13 (string) len=1 "{" -// template_string.baml:2:15 (string) len=4 "name" -// template_string.baml:2:20 (string) len=1 "}" -// template_string.baml:2:21 (string) len=1 "}" -// template_string.baml:2:22 (string) len=1 "!" -// template_string.baml:3:1 (string) len=1 "\"" -// template_string.baml:3:2 (string) len=1 "#" -// template_string.baml:5:1 (keyword) len=15 "template_string" -// template_string.baml:5:17 (function) [declaration] len=8 "NoParams" -// template_string.baml:5:28 (string) len=1 "#" -// template_string.baml:5:29 (string) len=1 "\"" -// template_string.baml:6:5 (string) len=1 "A" -// template_string.baml:6:7 (string) len=6 "static" -// template_string.baml:6:14 (string) len=8 "template" -// template_string.baml:6:22 (string) len=1 "." -// template_string.baml:7:1 (string) len=1 "\"" -// template_string.baml:7:2 (string) len=1 "#" +// template_string.baml:1:1 (keyword) len=8 "function" +// template_string.baml:1:10 (function) [declaration] len=8 "Greeting" +// template_string.baml:1:19 (parameter) [declaration] len=4 "name" +// template_string.baml:1:25 (type) [defaultLibrary] len=6 "string" +// template_string.baml:1:36 (type) [defaultLibrary] len=6 "string" +// template_string.baml:2:3 (string) len=1 "`" +// template_string.baml:2:4 (string) len=5 "Hello" +// template_string.baml:2:9 (string) len=1 "," +// template_string.baml:2:13 (parameter) len=4 "name" +// template_string.baml:2:18 (string) len=1 "!" +// template_string.baml:2:19 (string) len=1 "`" +// template_string.baml:5:1 (keyword) len=8 "function" +// template_string.baml:5:10 (function) [declaration] len=8 "NoParams" +// template_string.baml:5:24 (type) [defaultLibrary] len=6 "string" +// template_string.baml:6:3 (string) len=1 "`" +// template_string.baml:6:4 (string) len=1 "A" +// template_string.baml:6:6 (string) len=6 "static" +// template_string.baml:6:13 (string) len=8 "template" +// template_string.baml:6:21 (string) len=1 "." +// template_string.baml:6:22 (string) len=1 "`" diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string_calls.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string_calls.baml index f3cc06b5e71..48b107d22a6 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string_calls.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/semantic_tokens/template_string_calls.baml @@ -1,56 +1,43 @@ -template_string WithParams(a: int) #" - ... -"# +function WithParams(a: int) -> string { + `Value: ${a}` +} -template_string GoodCall1 #" - {{ WithParams(a=2) }} -"# +function GoodCall1() -> string { + `${WithParams(a = 2)}` +} -template_string GoodCall2 #" - {{ WithParams(2) }} -"# +function GoodCall2() -> string { + `${WithParams(2)}` +} //---- //- diagnostics // // //- semantic_tokens -// template_string_calls.baml:1:1 (keyword) len=15 "template_string" -// template_string_calls.baml:1:17 (function) [declaration] len=10 "WithParams" -// template_string_calls.baml:1:28 (parameter) [declaration] len=1 "a" -// template_string_calls.baml:1:31 (type) [defaultLibrary] len=3 "int" -// template_string_calls.baml:1:36 (string) len=1 "#" -// template_string_calls.baml:1:37 (string) len=1 "\"" -// template_string_calls.baml:2:3 (string) len=3 "..." -// template_string_calls.baml:3:1 (string) len=1 "\"" -// template_string_calls.baml:3:2 (string) len=1 "#" -// template_string_calls.baml:5:1 (keyword) len=15 "template_string" -// template_string_calls.baml:5:17 (function) [declaration] len=9 "GoodCall1" -// template_string_calls.baml:5:27 (string) len=1 "#" -// template_string_calls.baml:5:28 (string) len=1 "\"" -// template_string_calls.baml:6:3 (string) len=1 "{" -// template_string_calls.baml:6:4 (string) len=1 "{" -// template_string_calls.baml:6:6 (string) len=10 "WithParams" -// template_string_calls.baml:6:16 (string) len=1 "(" -// template_string_calls.baml:6:17 (string) len=1 "a" -// template_string_calls.baml:6:18 (string) len=1 "=" -// template_string_calls.baml:6:19 (string) len=1 "2" -// template_string_calls.baml:6:20 (string) len=1 ")" -// template_string_calls.baml:6:22 (string) len=1 "}" -// template_string_calls.baml:6:23 (string) len=1 "}" -// template_string_calls.baml:7:1 (string) len=1 "\"" -// template_string_calls.baml:7:2 (string) len=1 "#" -// template_string_calls.baml:9:1 (keyword) len=15 "template_string" -// template_string_calls.baml:9:17 (function) [declaration] len=9 "GoodCall2" -// template_string_calls.baml:9:27 (string) len=1 "#" -// template_string_calls.baml:9:28 (string) len=1 "\"" -// template_string_calls.baml:10:3 (string) len=1 "{" -// template_string_calls.baml:10:4 (string) len=1 "{" -// template_string_calls.baml:10:6 (string) len=10 "WithParams" -// template_string_calls.baml:10:16 (string) len=1 "(" -// template_string_calls.baml:10:17 (string) len=1 "2" -// template_string_calls.baml:10:18 (string) len=1 ")" -// template_string_calls.baml:10:20 (string) len=1 "}" -// template_string_calls.baml:10:21 (string) len=1 "}" -// template_string_calls.baml:11:1 (string) len=1 "\"" -// template_string_calls.baml:11:2 (string) len=1 "#" +// template_string_calls.baml:1:1 (keyword) len=8 "function" +// template_string_calls.baml:1:10 (function) [declaration] len=10 "WithParams" +// template_string_calls.baml:1:21 (parameter) [declaration] len=1 "a" +// template_string_calls.baml:1:24 (type) [defaultLibrary] len=3 "int" +// template_string_calls.baml:1:32 (type) [defaultLibrary] len=6 "string" +// template_string_calls.baml:2:3 (string) len=1 "`" +// template_string_calls.baml:2:4 (string) len=5 "Value" +// template_string_calls.baml:2:9 (string) len=1 ":" +// template_string_calls.baml:2:13 (parameter) len=1 "a" +// template_string_calls.baml:2:15 (string) len=1 "`" +// template_string_calls.baml:5:1 (keyword) len=8 "function" +// template_string_calls.baml:5:10 (function) [declaration] len=9 "GoodCall1" +// template_string_calls.baml:5:25 (type) [defaultLibrary] len=6 "string" +// template_string_calls.baml:6:3 (string) len=1 "`" +// template_string_calls.baml:6:6 (function) len=10 "WithParams" +// template_string_calls.baml:6:17 (parameter) len=1 "a" +// template_string_calls.baml:6:19 (operator) len=1 "=" +// template_string_calls.baml:6:21 (number) len=1 "2" +// template_string_calls.baml:6:24 (string) len=1 "`" +// template_string_calls.baml:9:1 (keyword) len=8 "function" +// template_string_calls.baml:9:10 (function) [declaration] len=9 "GoodCall2" +// template_string_calls.baml:9:25 (type) [defaultLibrary] len=6 "string" +// template_string_calls.baml:10:3 (string) len=1 "`" +// template_string_calls.baml:10:6 (function) len=10 "WithParams" +// template_string_calls.baml:10:17 (number) len=1 "2" +// template_string_calls.baml:10:20 (string) len=1 "`" diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_valid.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_valid.baml index bbce0de02f7..0890ee91b6d 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_valid.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_valid.baml @@ -14,10 +14,10 @@ client MyClient { function TestRemap(input: string) -> string { client MyClient - prompt #" - {{ _.chat("user") }} - Test message: {{ input }} - "# + prompt ` + ${role("user")} + Test message: ${input} + ` } //---- @@ -42,16 +42,10 @@ function TestRemap(input: string) -> string { // 13 │ ├─▶ } // · ╰──── replace with `client Name = ;` // ╰──── -// E0010 +// E0003 // -// × LLM function `TestRemap` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` -// ╭─[client_remap_role_valid.baml:15:45] -// 15 │ ╭─▶ function TestRemap(input: string) -> string { -// 16 │ │ client MyClient -// 17 │ │ prompt #" -// 18 │ │ {{ _.chat("user") }} -// 19 │ │ Test message: {{ input }} -// 20 │ │ "# -// 21 │ ├─▶ } -// · ╰──── migrate to a backtick prompt +// × unresolved name: `MyClient` +// ╭─[client_remap_role_valid.baml:16:10] +// 16 │ client MyClient +// · ──────── // ╰──── diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_with_default_fallback.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_with_default_fallback.baml index b713db606f1..26da6d3829e 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_with_default_fallback.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/client/remap_role_with_default_fallback.baml @@ -12,10 +12,10 @@ client MyClient { function TestRemapWithFallback(input: string) -> string { client MyClient - prompt #" - {{ _.chat("assistant") }} - This should fallback to system role and then be remapped to instructions: {{ input }} - "# + prompt ` + ${role("assistant")} + This should fallback to system role and then be remapped to instructions: ${input} + ` } //---- @@ -38,16 +38,10 @@ function TestRemapWithFallback(input: string) -> string { // 11 │ ├─▶ } // · ╰──── replace with `client Name = ;` // ╰──── -// E0010 +// E0003 // -// × LLM function `TestRemapWithFallback` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` -// ╭─[client_remap_role_with_default_fallback.baml:13:57] -// 13 │ ╭─▶ function TestRemapWithFallback(input: string) -> string { -// 14 │ │ client MyClient -// 15 │ │ prompt #" -// 16 │ │ {{ _.chat("assistant") }} -// 17 │ │ This should fallback to system role and then be remapped to instructions: {{ input }} -// 18 │ │ "# -// 19 │ ├─▶ } -// · ╰──── migrate to a backtick prompt +// × unresolved name: `MyClient` +// ╭─[client_remap_role_with_default_fallback.baml:14:10] +// 14 │ client MyClient +// · ──────── // ╰──── diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/dictionary/valid_dictionary.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/dictionary/valid_dictionary.baml index f609f1ab7a8..516c9b826f3 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/dictionary/valid_dictionary.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/dictionary/valid_dictionary.baml @@ -46,7 +46,7 @@ client MyClient { ] block_string #" hello there my friend - {# a comment in a prompt #} + raw strings preserve their contents "# } } @@ -106,7 +106,7 @@ client MyClient { // 46 │ │ ] // 47 │ │ block_string #" // 48 │ │ hello there my friend -// 49 │ │ {# a comment in a prompt #} +// 49 │ │ raw strings preserve their contents // 50 │ │ "# // 51 │ │ } // 52 │ ├─▶ } diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/functions_v2/prompt_errors/prompt1.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/functions_v2/prompt_errors/prompt1.baml index ecc2228764a..736e277c3b5 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/functions_v2/prompt_errors/prompt1.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/functions_v2/prompt_errors/prompt1.baml @@ -1,12 +1,12 @@ client Bar = openai.OpenAiClient.new(model = "gpt-4o"); -template_string Foo() #" - This! {{ b}} -"# +function Foo() -> string { + `This! ${b}` +} -template_string Foo2() #" - Also This! -"# +function Foo2() -> string { + `Also This!` +} function FooBar(a: string, b: int | bool) -> int { client Bar @@ -32,10 +32,10 @@ function Bar1(a: string) -> int { //---- //- diagnostics -// E0070 +// E0003 // -// ⚠ Variable `b` does not exist. Did you mean one of these: `_`, `baml`, `ctx`? +// × unresolved name: `b` // ╭─[functions_v2_prompt_errors_prompt1.baml:4:12] -// 4 │ This! {{ b}} +// 4 │ `This! ${b}` // · ─ // ╰──── diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/bad_calls.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/bad_calls.baml index 8d8836037fb..eb0f68e2d23 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/bad_calls.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/bad_calls.baml @@ -1,118 +1,51 @@ -template_string WithParams(a: int) #" - ... -"# +function WithParams(a: int) -> string { + `Value: ${a}` +} -template_string BadCall1 #" - {{ WithParams(a=2, b=2) }} -"# +function BadCall1() -> string { + `${WithParams(a = 2, b = 2)}` +} -template_string BadCall2 #" - {{ WithParams("a") }} -"# +function BadCall2() -> string { + `${WithParams("a")}` +} -template_string BadCall3 #" - {{ WithParams() }} -"# +function BadCall3() -> string { + `${WithParams()}` +} -template_string BadCall4 #" - {{ Random(2) }} -"# - -// warning: Function 'WithParams' expects 1 arguments, but got 2 -// --> template_string/bad_calls.baml:6 -// | -// 5 | template_string BadCall1 #" -// 6 | {{ WithParams(a=2, b=2) }} -// | -// warning: Function 'WithParams' expects argument 'a' to be of type int, but got literal["a"] -// --> template_string/bad_calls.baml:10 -// | -// 9 | template_string BadCall2 #" -// 10 | {{ WithParams("a") }} -// | -// warning: Function 'WithParams' expects 1 arguments, but got 0 -// --> template_string/bad_calls.baml:14 -// | -// 13 | template_string BadCall3 #" -// 14 | {{ WithParams() }} -// | -// warning: Variable `Random` does not exist. Did you mean one of these: `_`, `ctx`? -// --> template_string/bad_calls.baml:18 -// | -// 17 | template_string BadCall4 #" -// 18 | {{ Random(2) }} -// | -// warning: 'Random' is undefined, expected function -// --> template_string/bad_calls.baml:18 -// | -// 17 | template_string BadCall4 #" -// 18 | {{ Random(2) }} -// | -// warning: Function 'WithParams' expects 1 arguments, but got 2 -// --> template_string/bad_calls.baml:6 -// | -// 5 | template_string BadCall1 #" -// 6 | {{ WithParams(a=2, b=2) }} -// | -// warning: Function 'WithParams' expects argument 'a' to be of type int, but got literal["a"] -// --> template_string/bad_calls.baml:10 -// | -// 9 | template_string BadCall2 #" -// 10 | {{ WithParams("a") }} -// | -// warning: Function 'WithParams' expects 1 arguments, but got 0 -// --> template_string/bad_calls.baml:14 -// | -// 13 | template_string BadCall3 #" -// 14 | {{ WithParams() }} -// | -// warning: Variable `Random` does not exist. Did you mean one of these: `_`, `ctx`? -// --> template_string/bad_calls.baml:18 -// | -// 17 | template_string BadCall4 #" -// 18 | {{ Random(2) }} -// | -// warning: 'Random' is undefined, expected function -// --> template_string/bad_calls.baml:18 -// | -// 17 | template_string BadCall4 #" -// 18 | {{ Random(2) }} -// | +function BadCall4() -> string { + `${Random(2)}` +} //---- //- diagnostics -// E0080 +// E0005 // -// ⚠ Function 'WithParams' expects 1 arguments, but got 2 -// ╭─[template_string_bad_calls.baml:6:6] -// 6 │ {{ WithParams(a=2, b=2) }} -// · ──────────────────── +// × unknown named argument `b` +// ╭─[template_string_bad_calls.baml:6:28] +// 6 │ `${WithParams(a = 2, b = 2)}` +// · ─ // ╰──── -// E0083 +// E0001 // -// ⚠ Function 'WithParams' expects argument 'a' to be of type int, but got literal["a"] -// ╭─[template_string_bad_calls.baml:10:6] -// 10 │ {{ WithParams("a") }} -// · ─────────────── +// × mismatched types +// ╭─[template_string_bad_calls.baml:10:17] +// 10 │ `${WithParams("a")}` +// · ─┬─ +// · ╰── expected `int`, found `"a"` // ╰──── -// E0080 +// E0005 // -// ⚠ Function 'WithParams' expects 1 arguments, but got 0 +// × expected 1 argument(s), got 0 // ╭─[template_string_bad_calls.baml:14:6] -// 14 │ {{ WithParams() }} +// 14 │ `${WithParams()}` // · ──────────── // ╰──── -// E0070 +// E0003 // -// ⚠ Variable `Random` does not exist. Did you mean one of these: `_`, `baml`, `ctx`? +// × unresolved name: `Random` // ╭─[template_string_bad_calls.baml:18:6] -// 18 │ {{ Random(2) }} +// 18 │ `${Random(2)}` // · ────── // ╰──── -// E0073 -// -// ⚠ 'Random' is undefined, expected function -// ╭─[template_string_bad_calls.baml:18:6] -// 18 │ {{ Random(2) }} -// · ───────── -// ╰──── diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/good_calls.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/good_calls.baml index f0dd7ee224b..610b53b2e5e 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/good_calls.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/good_calls.baml @@ -1,14 +1,14 @@ -template_string WithParams(a: int) #" - ... -"# +function WithParams(a: int) -> string { + `Value: ${a}` +} -template_string GoodCall1 #" - {{ WithParams(a=2) }} -"# +function GoodCall1() -> string { + `${WithParams(a = 2)}` +} -template_string GoodCall2 #" - {{ WithParams(2) }} -"# +function GoodCall2() -> string { + `${WithParams(2)}` +} //---- //- diagnostics diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/invalid.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/invalid.baml index 1eb35c87f3e..f06ab78038e 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/invalid.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/invalid.baml @@ -1,38 +1,24 @@ -template_string FunctionWithBadParams( +function FunctionWithBadParams( param: Unknown, param2: Unknown2[], param3: string -) #" - {{ param.foo }} - {{ param2[0].doc }} - {{ param3 }} -"# - -// warning: 'param' is undefined, expected class -// --> template_string/invalid.baml:6 -// | -// 5 | ) #" -// 6 | {{ param.foo }} -// | -// warning: 'param' is undefined, expected class -// --> template_string/invalid.baml:6 -// | -// 5 | ) #" -// 6 | {{ param.foo }} -// | -// error: Type `Unknown` does not exist. Did you mean one of these: `int`, `float`, `bool`, `string`, `true`, `false`? -// --> template_string/invalid.baml:2 -// | -// 1 | template_string FunctionWithBadParams( -// 2 | param: Unknown, -// | -// error: Type `Unknown2` does not exist. Did you mean one of these: `string`, `int`, `float`, `bool`, `true`, `false`? -// --> template_string/invalid.baml:3 -// | -// 2 | param: Unknown, -// 3 | param2: Unknown2[], -// | +) -> string { + `${param.foo} ${param2[0].doc} ${param3}` +} //---- //- diagnostics -// +// E0002 +// +// × unresolved type: Unknown +// ╭─[template_string_invalid.baml:2:12] +// 2 │ param: Unknown, +// · ─────── +// ╰──── +// E0002 +// +// × unresolved type: Unknown2 +// ╭─[template_string_invalid.baml:3:13] +// 3 │ param2: Unknown2[], +// · ────────── +// ╰──── diff --git a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/valid.baml b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/valid.baml index b27cd3fa66e..3d44ed2eabb 100644 --- a/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/valid.baml +++ b/baml_language/crates/baml_lsp2_actions_tests/test_files/syntax/template_string/valid.baml @@ -1,10 +1,10 @@ -template_string SomeString #" - ... -"# +function SomeString() -> string { + `...` +} -template_string WithParams(a: int) #" - ... -"# +function WithParams(a: int) -> string { + `Value: ${a}` +} //---- //- diagnostics diff --git a/baml_language/crates/baml_project/src/client_codegen.rs b/baml_language/crates/baml_project/src/client_codegen.rs index 51b0c7463c5..07365f4e509 100644 --- a/baml_language/crates/baml_project/src/client_codegen.rs +++ b/baml_language/crates/baml_project/src/client_codegen.rs @@ -933,7 +933,7 @@ client GPT4 { function Extract(client: string, text: string) -> string { client GPT4 - prompt #"{{ text }}"# + prompt `${text}` } "##, ); diff --git a/baml_language/crates/baml_project/src/db.rs b/baml_language/crates/baml_project/src/db.rs index d52774a118f..0324467cea4 100644 --- a/baml_language/crates/baml_project/src/db.rs +++ b/baml_language/crates/baml_project/src/db.rs @@ -2544,7 +2544,7 @@ function Workflow(input: string) -> string { r##" function Summarize(input: string) -> string { client GPT4 - prompt #"Summarize {{ input }}"# + prompt `Summarize ${input}` } "##, ); @@ -2689,7 +2689,7 @@ function GuessingGame() -> string { r##" function Summarize(input: string) -> string { client GPT4 - prompt #"Summarize {{ input }}"# + prompt `Summarize ${input}` } function Workflow(input: string) -> string { @@ -3027,12 +3027,12 @@ test "renders workflow" { let source = r##" function Summarize(input: string) -> string { client GPT4 - prompt #"Summarize {{ input }}"# + prompt `Summarize ${input}` } "##; db.add_or_update_file(path, source); - for needle in ["client", "GPT4", "prompt", "Summarize {{ input }}"] { + for needle in ["client", "GPT4", "prompt", "Summarize ${input}"] { let offset = u32::try_from(source.find(needle).expect("needle exists")).unwrap(); let ctx = db.playground_cursor_context(path.to_str().unwrap(), offset); diff --git a/baml_language/crates/baml_surface/src/handles_tests.rs b/baml_language/crates/baml_surface/src/handles_tests.rs index 071cc417e86..3906139c928 100644 --- a/baml_language/crates/baml_surface/src/handles_tests.rs +++ b/baml_language/crates/baml_surface/src/handles_tests.rs @@ -40,7 +40,6 @@ type Id = string function greet(name: string) -> string { name } -template_string Prompt(x: string) #"{{ x }}"# client Fast { provider openai diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/header_in_llm_function/test.baml b/baml_language/crates/baml_tests/projects/broken_syntax/header_in_llm_function/test.baml index 4b92331a3f3..5e351ccf2d1 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/header_in_llm_function/test.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/header_in_llm_function/test.baml @@ -13,8 +13,8 @@ function FetchDatax(user_id: string) -> Hi { //# collect source material client VertexGCP - prompt #" - give short story {{ user_id }} - {{ ctx.output_format }} - "# + prompt ` + give short story ${user_id} + ${ctx.output_format} + ` } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/ambiguous_function.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/ambiguous_function.baml index 7d5d1009a5e..9e5598e4a54 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/ambiguous_function.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/ambiguous_function.baml @@ -7,5 +7,5 @@ function Ambiguous(input: string) -> string { // This one has a typo but intent is clear function AnotherLLM(input: string) -> string { clent GPT4 // Typo in "client" - prompt #"Process: {{ input }}"# // "prompt" keyword helps disambiguate + prompt `Process: ${input}` // "prompt" keyword helps disambiguate } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/llm_function.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/llm_function.baml index c2fc204352d..fea9fe6f728 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/llm_function.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/llm_function.baml @@ -1,7 +1,7 @@ function ExtractData(text: string) -> JsonData { client GPT4 - prompt #" - Extract structured data from: {{ text }} + prompt ` + Extract structured data from: ${text} Return as JSON. - "# + ` } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/mixed_functions.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/mixed_functions.baml index 520c23bb1ca..94fc4fd060f 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/mixed_functions.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_speculative/mixed_functions.baml @@ -1,12 +1,12 @@ // Mix of LLM and expression functions in one file function LLMAnalyze(text: string) -> Analysis { client GPT4 - prompt #" + prompt ` Analyze the following text: - {{ text }} + ${text} Return an Analysis object with sentiment and keywords. - "# + ` } class Analysis { @@ -30,12 +30,12 @@ function ProcessAnalysis(analysis: Analysis) -> string { function ChainedProcess(input: string) -> string { client GPT4 - prompt #" + prompt ` Process this through multiple steps: - 1. {{ input }} + 1. ${input} 2. Transform it 3. Return result - "# + ` } function SimpleCalc(a: int, b: int) -> int { diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/nested_quotes.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/nested_quotes.baml index 499a938ec5b..606b5d563ad 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/nested_quotes.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/nested_quotes.baml @@ -10,8 +10,8 @@ class NestedQuotes { // Test quotes in template strings function FormatMessage(name: string) -> string { client GPT4 - prompt #" - Generate a message for "{{ name }}" - Include quotes like: "Welcome, '{{ name }}'!" - "# + prompt ` + Generate a message for "${name}" + Include quotes like: "Welcome, '${name}'!" + ` } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/raw_strings.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/raw_strings.baml index 4e3fcf169dd..51159bfe3f1 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/raw_strings.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/raw_strings.baml @@ -12,10 +12,10 @@ class RawStrings { function ProcessText(input: string) -> string { client GPT4 - prompt #" + prompt ` Process the following text: - {{ input }} + ${input} Return the processed result. - "# + ` } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/unicode_strings.baml b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/unicode_strings.baml index 0985b5e0923..714eb2b2d3f 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/unicode_strings.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/parser_strings/unicode_strings.baml @@ -24,8 +24,8 @@ class UnicodeStrings { // Test Unicode in function names and prompts function 处理文本(输入: string) -> string { client GPT4 - prompt #" - Process the text: {{ 输入 }} + prompt ` + Process the text: ${输入} Return result with emojis: ✅ or ❌ - "# + ` } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/pending_greaters_fix/extra_greater.baml b/baml_language/crates/baml_tests/projects/broken_syntax/pending_greaters_fix/extra_greater.baml index 9a0b2068665..a5018938802 100644 --- a/baml_language/crates/baml_tests/projects/broken_syntax/pending_greaters_fix/extra_greater.baml +++ b/baml_language/crates/baml_tests/projects/broken_syntax/pending_greaters_fix/extra_greater.baml @@ -22,13 +22,13 @@ class NestedGenerics { // Function parameter with extra > function BadParam(x: map>) -> string { client GPT4 - prompt #"test"# + prompt `test` } // Function return type with extra > function BadReturnType() -> map> { client GPT4 - prompt #"test"# + prompt `test` } // Type alias with extra > diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml b/baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml new file mode 100644 index 00000000000..ceab33bbfb9 --- /dev/null +++ b/baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml @@ -0,0 +1 @@ +template_string Greeting(name: string) `Hello ${name}` diff --git a/baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml b/baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml index 1a58bf243af..787727ef7c8 100644 --- a/baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml +++ b/baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml @@ -1,6 +1,5 @@ -// BEP-049 backtick string literals in the declarative slots that the formatter -// used to reject (it only knew the quoted and raw string tokens there). Both -// slots below compile cleanly and format without error. +// BEP-049 backtick string literals in prompt and expression positions compile +// cleanly and format without error. // // `baml fmt` re-indents a multi-line backtick interior to sit one level past the // surrounding block (like a raw string), but only when that preserves the @@ -20,7 +19,9 @@ function Greet(name: string) -> string { ` } -// Backtick body in a `template_string`. -template_string Header(title: string) ` - # ${title} -` +// Backtick value in expression position. +function Header(title: string) -> string { + ` + # ${title} + ` +} diff --git a/baml_language/crates/baml_tests/projects/compiles/string_methods/string_methods.baml b/baml_language/crates/baml_tests/projects/compiles/string_methods/string_methods.baml index 6996b52065a..6003fb8100c 100644 --- a/baml_language/crates/baml_tests/projects/compiles/string_methods/string_methods.baml +++ b/baml_language/crates/baml_tests/projects/compiles/string_methods/string_methods.baml @@ -1,11 +1,9 @@ -// Test: string method calls inside backtick template holes produce no -// diagnostics. (Jinja template_string bodies went away with the legacy LLM -// path; templates are backticks with `${...}` holes now.) +// Test: string method calls inside backtick interpolation produce no diagnostics. -template_string Upper(s: string) `${s.to_upper_case()}` +function Upper(s: string) -> string { `${s.to_upper_case()}` } -template_string StartsWith(s: string) `${s.starts_with("hello")}` +function StartsWith(s: string) -> string { `${s.starts_with("hello")}` } -template_string SplitCount(s: string) `${s.split(",").length()}` +function SplitCount(s: string) -> string { `${s.split(",").length()}` } -template_string Trimmed(s: string) `${s.trim()}` +function Trimmed(s: string) -> string { `${s.trim()}` } diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/client_option_types/client_option_types.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/client_option_types/client_option_types.baml index 6585116c2dc..3e6a088f98b 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/client_option_types/client_option_types.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/client_option_types/client_option_types.baml @@ -21,14 +21,14 @@ retry_policy LegacyRetry { max_retries 3 } -// Removed: Jinja `#"..."#` prompt on an LLM function. -function LegacyJinjaPrompt(input: string) -> string { +// Jinja prompt migrated to a backtick prompt. +function MigratedPrompt(input: string) -> string { client ValidClient - prompt #"Echo: {{ input }}"# + prompt `Echo: ${input}` } // Structural role markers remain valid while the surrounding fixture checks -// the independently removed client/Jinja forms. +// the independently removed client forms. function LegacyRoleMarker(input: string) -> string { client ValidClient prompt `${role("system")}Be terse. ${input}` diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/config_decls.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/config_decls.baml index f683c2ee0c6..3f9ba5af3a9 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/config_decls.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/config_decls.baml @@ -444,7 +444,7 @@ TriviaRetryBlock /* 4 name trailing */ // ===== Test declaration ===== function TestTarget() -> string { client BasicClient - prompt #"Hello"# + prompt `Hello` } test BasicTest { diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/template_string_decls.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/template_string_decls.baml index 0ae6c91331c..30e3abad850 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/template_string_decls.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/format_checks/template_string_decls.baml @@ -1,373 +1,65 @@ -// Testing all the formatter cases for template string declarations -// as well as any special cases inside the template string - -// ===== Basic template string ===== -template_string BasicTemplate(name: string) #" - Hello, {{ name }}! -"# - -// ===== Template string with no params ===== -template_string NoParams() #" - A static template with no parameters. -"# - -// ===== Template string with multiple params ===== -template_string MultiParam(first: string, last: string, age: int) #" - Name: {{ first }} {{ last }} - Age: {{ age }} -"# - -// ===== Template string with complex param types ===== -template_string ComplexParams(items: string[], config: map) #" - Items: {{ items }} - Config: {{ config }} -"# - -// ===== Template string with single-line body ===== -template_string SingleLine(x: int) #"Value is {{ x }}"# - -// ===== Template string with many params that should wrap ===== -template_string ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} {{ fourth_parameter }} -"# - -// ===== Very long param list ===== -template_string VeryLongParams(extremely_long_parameter_name: string, another_very_long_parameter_name: int) #" - {{ extremely_long_parameter_name }} {{ another_very_long_parameter_name }} -"# - -// ===== Template with template comments ===== -template_string WithComment(x: string) #" - {# This is a template comment #} - Value: {{ x }} -"# - -// ===== Trivia on template string declaration (line comments) ===== -// 1 keyword leading -template_string // 2 keyword trailing -// 3 name leading -TriviaTemplate // 4 name trailing -// 5 open paren leading -( // 6 open paren trailing -// 7 param leading -x // 8 param name trailing -// 9 colon leading -: // 10 colon trailing -// 11 type leading -string // 12 type trailing -// 13 close paren leading -) // 14 close paren trailing -// 15 body leading -#" - Hello {{ x }} -"# - -/////////////////// - -// ===== Trivia on template string declaration (block comments) ===== -/* 1 keyword leading */ -template_string /* 2 keyword trailing */ -/* 3 name leading */ -TriviaTemplateBlock /* 4 name trailing */ -/* 5 open paren leading */ -( /* 6 open paren trailing */ -/* 7 param leading */ -x /* 8 param name trailing */ -: /* 9 colon trailing */ -/* 10 type leading */ -string /* 11 type trailing */ -/* 12 close paren leading */ -) /* 13 close paren trailing */ -/* 14 body leading */ -#" - Hello {{ x }} -"# - -/////////////////// - -// ===== Multi param trivia (line comments) ===== -template_string MultiParamTrivia( - // 1 first param leading - a // 2 first param trailing - : // 3 first colon trailing - // 4 first type leading - string // 5 first type trailing - , // 6 comma trailing - // 7 second param leading - b // 8 second param trailing - : // 9 second colon trailing - // 10 second type leading - int // 11 second type trailing -) #" - {{ a }} {{ b }} -"# - -// ===== Multi param trivia (block comments) ===== -template_string MultiParamTriviaBlock( - /* 1 first param leading */ - a /* 2 first param trailing */ - : /* 3 first colon trailing */ - /* 4 first type leading */ - string /* 5 first type trailing */ - , /* 6 comma trailing */ - /* 7 second param leading */ - b /* 8 second param trailing */ - : /* 9 second colon trailing */ - /* 10 second type leading */ - int /* 11 second type trailing */ -) #" - {{ a }} {{ b }} -"# - -// ===== Short name ===== -template_string T(x: int) #"{{ x }}"# - -// ===== Long name exceeding limit ===== -template_string ThisIsAnExtremelyLongTemplateStringNameThatDefinitelyExceedsTheDefaultLineLimitOfOneHundredColumns(x: int) #" - {{ x }} -"# - -// ===== Long name + many params exceeding limit ===== -template_string VeryLongTemplateNameWithManyParameters(first_parameter: string, second_parameter: int, third_parameter: bool, fourth_parameter: float) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} {{ fourth_parameter }} -"# - -// ===== Many params with complex types exceeding limit ===== -template_string ComplexTypesWrap(items: (int | string | bool)[], config: map, callback: (x: int, y: int) -> string) #" - {{ items }} {{ config }} -"# - -// ===== Very long param list with many complex params far exceeding limit ===== -template_string VeryLongComplexParams(first_parameter: map, second_parameter: (int | string)[], third_parameter: map>, fourth_parameter: (x: int) -> string) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -// ===== Params with deeply nested generic types exceeding limit ===== -template_string DeepGenericParams(data: map>>) #" - {{ data }} -"# - -// ===== Params with union types ===== -template_string UnionParams(result: string | int | bool | null, items: (string | int)[]) #" - {{ result }} {{ items }} -"# - -// ===== Params with optional types ===== -template_string OptionalParams(name: string?, age: int?, items: string[]?) #" - {{ name }} {{ age }} {{ items }} -"# - -// ===== Params with function types ===== -template_string FnTypeParams(transform: (x: int) -> int, combine: (a: string, b: string) -> string) #" - transformed -"# - -// ===== Long name + long complex params far exceeding limit ===== -template_string ExtremelyLongNamePlusComplexParameters(first_very_long_parameter_name: map, second_very_long_parameter_name: (int | string | bool | float | null)[]) #" - {{ first_very_long_parameter_name }} {{ second_very_long_parameter_name }} -"# - -// ===== Six params all with long names and types exceeding limit ===== -template_string SixLongParams(first_param: string, second_param: int, third_param: bool, fourth_param: float, fifth_param: string[], sixth_param: map) #" - {{ first_param }} {{ second_param }} {{ third_param }} {{ fourth_param }} {{ fifth_param }} {{ sixth_param }} -"# - -// ===== Trivia on wrapping many params (line comments) ===== -// 1 keyword leading -template_string // 2 keyword trailing -// 3 name leading -TriviaWrappingParams // 4 name trailing -// 5 open paren leading -( // 6 open paren trailing -// 7 first param leading -first_parameter // 8 first name trailing -: // 9 first colon trailing -// 10 first type leading -map // 11 first type trailing -, // 12 comma trailing -// 13 second param leading -second_parameter // 14 second name trailing -: // 15 second colon trailing -// 16 second type leading -(int | string)[] // 17 second type trailing -, // 18 comma trailing -// 19 third param leading -third_parameter // 20 third name trailing -: // 21 third colon trailing -// 22 third type leading -map> // 23 third type trailing -// 24 close paren leading -) // 25 close paren trailing -// 26 body leading -#" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping many params (block comments) ===== -/* 1 keyword leading */ -template_string /* 2 keyword trailing */ -/* 3 name leading */ -TriviaWrappingParamsBlock /* 4 name trailing */ -/* 5 open paren leading */ -( /* 6 open paren trailing */ -/* 7 first param leading */ -first_parameter /* 8 first name trailing */ -: /* 9 first colon trailing */ -/* 10 first type leading */ -map /* 11 first type trailing */ -, /* 12 comma trailing */ -/* 13 second param leading */ -second_parameter /* 14 second name trailing */ -: /* 15 second colon trailing */ -/* 16 second type leading */ -(int | string)[] /* 17 second type trailing */ -, /* 18 comma trailing */ -/* 19 third param leading */ -third_parameter /* 20 third name trailing */ -: /* 21 third colon trailing */ -/* 22 third type leading */ -map> /* 23 third type trailing */ -/* 24 close paren leading */ -) /* 25 close paren trailing */ -/* 26 body leading */ -#" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping long name + params (line comments) ===== -// 1 keyword leading -template_string // 2 keyword trailing -// 3 name leading -TriviaVeryLongTemplateNameWithParameters // 4 name trailing -// 5 open paren leading -( // 6 open paren trailing -// 7 first param leading -first_parameter // 8 first name trailing -: // 9 first colon trailing -// 10 first type leading -string // 11 first type trailing -, // 12 comma trailing -// 13 second param leading -second_parameter // 14 second name trailing -: // 15 second colon trailing -// 16 second type leading -int // 17 second type trailing -, // 18 comma trailing -// 19 third param leading -third_parameter // 20 third name trailing -: // 21 third colon trailing -// 22 third type leading -bool // 23 third type trailing -// 24 close paren leading -) // 25 close paren trailing -// 26 body leading -#" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping long name + params (block comments) ===== -/* 1 keyword leading */ -template_string /* 2 keyword trailing */ -/* 3 name leading */ -TriviaVeryLongTemplateNameWithParametersBlock /* 4 name trailing */ -/* 5 open paren leading */ -( /* 6 open paren trailing */ -/* 7 first param leading */ -first_parameter /* 8 first name trailing */ -: /* 9 first colon trailing */ -/* 10 first type leading */ -string /* 11 first type trailing */ -, /* 12 comma trailing */ -/* 13 second param leading */ -second_parameter /* 14 second name trailing */ -: /* 15 second colon trailing */ -/* 16 second type leading */ -int /* 17 second type trailing */ -, /* 18 comma trailing */ -/* 19 third param leading */ -third_parameter /* 20 third name trailing */ -: /* 21 third colon trailing */ -/* 22 third type leading */ -bool /* 23 third type trailing */ -/* 24 close paren leading */ -) /* 25 close paren trailing */ -/* 26 body leading */ -#" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on param with deeply nested generic type (line comments) ===== -template_string TriviaDeepGenericParam( - // 1 param leading - data // 2 name trailing - : // 3 colon trailing - // 4 type leading - map // 5 map trailing - < // 6 open angle trailing - // 7 key type leading - string // 8 key type trailing - , // 9 comma trailing - // 10 value type leading - map // 11 inner map trailing - < // 12 inner open angle trailing - // 13 inner key leading - string // 14 inner key trailing - , // 15 inner comma trailing - // 16 inner value leading - int // 17 inner value trailing - | // 18 pipe trailing - // 19 union member leading - string // 20 union member trailing - > // 21 inner close angle trailing - > // 22 outer close angle trailing -) #" - {{ data }} -"# - -// ===== Trivia on param with deeply nested generic type (block comments) ===== -template_string TriviaDeepGenericParamBlock( - /* 1 param leading */ - data /* 2 name trailing */ - : /* 3 colon trailing */ - /* 4 type leading */ - map /* 5 map trailing */ - < /* 6 open angle trailing */ - /* 7 key type leading */ - string /* 8 key type trailing */ - , /* 9 comma trailing */ - /* 10 value type leading */ - map /* 11 inner map trailing */ - < /* 12 inner open angle trailing */ - /* 13 inner key leading */ - string /* 14 inner key trailing */ - , /* 15 inner comma trailing */ - /* 16 inner value leading */ - int /* 17 inner value trailing */ - | /* 18 pipe trailing */ - /* 19 union member leading */ - string /* 20 union member trailing */ - > /* 21 inner close angle trailing */ - > /* 22 outer close angle trailing */ -) #" - {{ data }} -"# - -// ===== Extraneous whitespace ===== -template_string ExtraWhitespace ( a : string , b : int ) #" - {{ a }} {{ b }} -"# - -// ===== Template with control flow ===== -template_string WithControl(items: string[]) #" - {% for item in items %} - - {{ item }} - {% endfor %} -"# +// Formatter coverage migrated from template_string declarations to functions +// returning backtick strings. + +function BasicTemplate(name: string) -> string { + `Hello, ${name}!` +} + +function NoParams() -> string { + `A static template with no parameters.` +} + +function MultiParam(first: string, last: string, age: int) -> string { + ` + Name: ${first} ${last} + Age: ${age} + ` +} + +function ComplexParams(items: string[], config: map) -> string { + ` + Items: ${items} + Config: ${config} + ` +} + +function SingleLine(x: int) -> string { `Value is ${x}` } + +function ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) -> string { + `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` +} + +function ComplexTypesWrap(items: (int | string | bool)[], config: map, callback: (x: int, y: int) -> string) -> string { + `${items} ${config}` +} + +function OptionalParams(name: string?, age: int?, items: string[]?) -> string { + `${name} ${age} ${items}` +} + +function FnTypeParams(transform: (x: int) -> int, combine: (a: string, b: string) -> string) -> string { + `transformed` +} + +// Trivia around a function returning a backtick string. +function // keyword trailing +TriviaTemplate // name trailing +( // open paren trailing + x // parameter trailing + : string +) // close paren trailing +-> string { + `Hello ${x}` +} + +function WithControl(items: string[]) -> string { + ` + ${for (let item in items)} + - ${item} + ${endfor} + ` +} + +function WithConditional(show: bool, value: string) -> string { + `${if (show)}${value}${else}hidden${endif}` +} diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/jinja_prompt_removed/main.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/jinja_prompt_removed/main.baml new file mode 100644 index 00000000000..8000646d37a --- /dev/null +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/jinja_prompt_removed/main.baml @@ -0,0 +1,4 @@ +function Greet(name: string) -> string { + client "openai/gpt-4o" + prompt #"Hello {{ name }}"# +} diff --git a/baml_language/crates/baml_tests/projects/diagnostic_errors/optional_parameter_defaults/unsupported_contexts.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/optional_parameter_defaults/unsupported_contexts.baml index 581ee5183ed..0fc2545a6bc 100644 --- a/baml_language/crates/baml_tests/projects/diagnostic_errors/optional_parameter_defaults/unsupported_contexts.baml +++ b/baml_language/crates/baml_tests/projects/diagnostic_errors/optional_parameter_defaults/unsupported_contexts.baml @@ -1,4 +1,6 @@ -template_string Greeting(name: string = "world") #"hello {{ name }}"# +function Greeting(name: string = "world") -> string { + `hello ${name}` +} type NamedFilter = (query: string, filter?: string?) -> int throws never type NamedLimit = (query: string, limit?: string?) -> int throws never diff --git a/baml_language/crates/baml_tests/projects/parser_stress/large_file.baml b/baml_language/crates/baml_tests/projects/parser_stress/large_file.baml index cc908fbcd64..fba667776cc 100644 --- a/baml_language/crates/baml_tests/projects/parser_stress/large_file.baml +++ b/baml_language/crates/baml_tests/projects/parser_stress/large_file.baml @@ -6600,500 +6600,500 @@ enum Status100 { function Process1(input: string) -> string { client GPT4 - prompt "Process input 1: {{ input }}" + prompt `Process input 1: ${input}` } function Process2(input: string) -> string { client GPT4 - prompt "Process input 2: {{ input }}" + prompt `Process input 2: ${input}` } function Process3(input: string) -> string { client GPT4 - prompt "Process input 3: {{ input }}" + prompt `Process input 3: ${input}` } function Process4(input: string) -> string { client GPT4 - prompt "Process input 4: {{ input }}" + prompt `Process input 4: ${input}` } function Process5(input: string) -> string { client GPT4 - prompt "Process input 5: {{ input }}" + prompt `Process input 5: ${input}` } function Process6(input: string) -> string { client GPT4 - prompt "Process input 6: {{ input }}" + prompt `Process input 6: ${input}` } function Process7(input: string) -> string { client GPT4 - prompt "Process input 7: {{ input }}" + prompt `Process input 7: ${input}` } function Process8(input: string) -> string { client GPT4 - prompt "Process input 8: {{ input }}" + prompt `Process input 8: ${input}` } function Process9(input: string) -> string { client GPT4 - prompt "Process input 9: {{ input }}" + prompt `Process input 9: ${input}` } function Process10(input: string) -> string { client GPT4 - prompt "Process input 10: {{ input }}" + prompt `Process input 10: ${input}` } function Process11(input: string) -> string { client GPT4 - prompt "Process input 11: {{ input }}" + prompt `Process input 11: ${input}` } function Process12(input: string) -> string { client GPT4 - prompt "Process input 12: {{ input }}" + prompt `Process input 12: ${input}` } function Process13(input: string) -> string { client GPT4 - prompt "Process input 13: {{ input }}" + prompt `Process input 13: ${input}` } function Process14(input: string) -> string { client GPT4 - prompt "Process input 14: {{ input }}" + prompt `Process input 14: ${input}` } function Process15(input: string) -> string { client GPT4 - prompt "Process input 15: {{ input }}" + prompt `Process input 15: ${input}` } function Process16(input: string) -> string { client GPT4 - prompt "Process input 16: {{ input }}" + prompt `Process input 16: ${input}` } function Process17(input: string) -> string { client GPT4 - prompt "Process input 17: {{ input }}" + prompt `Process input 17: ${input}` } function Process18(input: string) -> string { client GPT4 - prompt "Process input 18: {{ input }}" + prompt `Process input 18: ${input}` } function Process19(input: string) -> string { client GPT4 - prompt "Process input 19: {{ input }}" + prompt `Process input 19: ${input}` } function Process20(input: string) -> string { client GPT4 - prompt "Process input 20: {{ input }}" + prompt `Process input 20: ${input}` } function Process21(input: string) -> string { client GPT4 - prompt "Process input 21: {{ input }}" + prompt `Process input 21: ${input}` } function Process22(input: string) -> string { client GPT4 - prompt "Process input 22: {{ input }}" + prompt `Process input 22: ${input}` } function Process23(input: string) -> string { client GPT4 - prompt "Process input 23: {{ input }}" + prompt `Process input 23: ${input}` } function Process24(input: string) -> string { client GPT4 - prompt "Process input 24: {{ input }}" + prompt `Process input 24: ${input}` } function Process25(input: string) -> string { client GPT4 - prompt "Process input 25: {{ input }}" + prompt `Process input 25: ${input}` } function Process26(input: string) -> string { client GPT4 - prompt "Process input 26: {{ input }}" + prompt `Process input 26: ${input}` } function Process27(input: string) -> string { client GPT4 - prompt "Process input 27: {{ input }}" + prompt `Process input 27: ${input}` } function Process28(input: string) -> string { client GPT4 - prompt "Process input 28: {{ input }}" + prompt `Process input 28: ${input}` } function Process29(input: string) -> string { client GPT4 - prompt "Process input 29: {{ input }}" + prompt `Process input 29: ${input}` } function Process30(input: string) -> string { client GPT4 - prompt "Process input 30: {{ input }}" + prompt `Process input 30: ${input}` } function Process31(input: string) -> string { client GPT4 - prompt "Process input 31: {{ input }}" + prompt `Process input 31: ${input}` } function Process32(input: string) -> string { client GPT4 - prompt "Process input 32: {{ input }}" + prompt `Process input 32: ${input}` } function Process33(input: string) -> string { client GPT4 - prompt "Process input 33: {{ input }}" + prompt `Process input 33: ${input}` } function Process34(input: string) -> string { client GPT4 - prompt "Process input 34: {{ input }}" + prompt `Process input 34: ${input}` } function Process35(input: string) -> string { client GPT4 - prompt "Process input 35: {{ input }}" + prompt `Process input 35: ${input}` } function Process36(input: string) -> string { client GPT4 - prompt "Process input 36: {{ input }}" + prompt `Process input 36: ${input}` } function Process37(input: string) -> string { client GPT4 - prompt "Process input 37: {{ input }}" + prompt `Process input 37: ${input}` } function Process38(input: string) -> string { client GPT4 - prompt "Process input 38: {{ input }}" + prompt `Process input 38: ${input}` } function Process39(input: string) -> string { client GPT4 - prompt "Process input 39: {{ input }}" + prompt `Process input 39: ${input}` } function Process40(input: string) -> string { client GPT4 - prompt "Process input 40: {{ input }}" + prompt `Process input 40: ${input}` } function Process41(input: string) -> string { client GPT4 - prompt "Process input 41: {{ input }}" + prompt `Process input 41: ${input}` } function Process42(input: string) -> string { client GPT4 - prompt "Process input 42: {{ input }}" + prompt `Process input 42: ${input}` } function Process43(input: string) -> string { client GPT4 - prompt "Process input 43: {{ input }}" + prompt `Process input 43: ${input}` } function Process44(input: string) -> string { client GPT4 - prompt "Process input 44: {{ input }}" + prompt `Process input 44: ${input}` } function Process45(input: string) -> string { client GPT4 - prompt "Process input 45: {{ input }}" + prompt `Process input 45: ${input}` } function Process46(input: string) -> string { client GPT4 - prompt "Process input 46: {{ input }}" + prompt `Process input 46: ${input}` } function Process47(input: string) -> string { client GPT4 - prompt "Process input 47: {{ input }}" + prompt `Process input 47: ${input}` } function Process48(input: string) -> string { client GPT4 - prompt "Process input 48: {{ input }}" + prompt `Process input 48: ${input}` } function Process49(input: string) -> string { client GPT4 - prompt "Process input 49: {{ input }}" + prompt `Process input 49: ${input}` } function Process50(input: string) -> string { client GPT4 - prompt "Process input 50: {{ input }}" + prompt `Process input 50: ${input}` } function Process51(input: string) -> string { client GPT4 - prompt "Process input 51: {{ input }}" + prompt `Process input 51: ${input}` } function Process52(input: string) -> string { client GPT4 - prompt "Process input 52: {{ input }}" + prompt `Process input 52: ${input}` } function Process53(input: string) -> string { client GPT4 - prompt "Process input 53: {{ input }}" + prompt `Process input 53: ${input}` } function Process54(input: string) -> string { client GPT4 - prompt "Process input 54: {{ input }}" + prompt `Process input 54: ${input}` } function Process55(input: string) -> string { client GPT4 - prompt "Process input 55: {{ input }}" + prompt `Process input 55: ${input}` } function Process56(input: string) -> string { client GPT4 - prompt "Process input 56: {{ input }}" + prompt `Process input 56: ${input}` } function Process57(input: string) -> string { client GPT4 - prompt "Process input 57: {{ input }}" + prompt `Process input 57: ${input}` } function Process58(input: string) -> string { client GPT4 - prompt "Process input 58: {{ input }}" + prompt `Process input 58: ${input}` } function Process59(input: string) -> string { client GPT4 - prompt "Process input 59: {{ input }}" + prompt `Process input 59: ${input}` } function Process60(input: string) -> string { client GPT4 - prompt "Process input 60: {{ input }}" + prompt `Process input 60: ${input}` } function Process61(input: string) -> string { client GPT4 - prompt "Process input 61: {{ input }}" + prompt `Process input 61: ${input}` } function Process62(input: string) -> string { client GPT4 - prompt "Process input 62: {{ input }}" + prompt `Process input 62: ${input}` } function Process63(input: string) -> string { client GPT4 - prompt "Process input 63: {{ input }}" + prompt `Process input 63: ${input}` } function Process64(input: string) -> string { client GPT4 - prompt "Process input 64: {{ input }}" + prompt `Process input 64: ${input}` } function Process65(input: string) -> string { client GPT4 - prompt "Process input 65: {{ input }}" + prompt `Process input 65: ${input}` } function Process66(input: string) -> string { client GPT4 - prompt "Process input 66: {{ input }}" + prompt `Process input 66: ${input}` } function Process67(input: string) -> string { client GPT4 - prompt "Process input 67: {{ input }}" + prompt `Process input 67: ${input}` } function Process68(input: string) -> string { client GPT4 - prompt "Process input 68: {{ input }}" + prompt `Process input 68: ${input}` } function Process69(input: string) -> string { client GPT4 - prompt "Process input 69: {{ input }}" + prompt `Process input 69: ${input}` } function Process70(input: string) -> string { client GPT4 - prompt "Process input 70: {{ input }}" + prompt `Process input 70: ${input}` } function Process71(input: string) -> string { client GPT4 - prompt "Process input 71: {{ input }}" + prompt `Process input 71: ${input}` } function Process72(input: string) -> string { client GPT4 - prompt "Process input 72: {{ input }}" + prompt `Process input 72: ${input}` } function Process73(input: string) -> string { client GPT4 - prompt "Process input 73: {{ input }}" + prompt `Process input 73: ${input}` } function Process74(input: string) -> string { client GPT4 - prompt "Process input 74: {{ input }}" + prompt `Process input 74: ${input}` } function Process75(input: string) -> string { client GPT4 - prompt "Process input 75: {{ input }}" + prompt `Process input 75: ${input}` } function Process76(input: string) -> string { client GPT4 - prompt "Process input 76: {{ input }}" + prompt `Process input 76: ${input}` } function Process77(input: string) -> string { client GPT4 - prompt "Process input 77: {{ input }}" + prompt `Process input 77: ${input}` } function Process78(input: string) -> string { client GPT4 - prompt "Process input 78: {{ input }}" + prompt `Process input 78: ${input}` } function Process79(input: string) -> string { client GPT4 - prompt "Process input 79: {{ input }}" + prompt `Process input 79: ${input}` } function Process80(input: string) -> string { client GPT4 - prompt "Process input 80: {{ input }}" + prompt `Process input 80: ${input}` } function Process81(input: string) -> string { client GPT4 - prompt "Process input 81: {{ input }}" + prompt `Process input 81: ${input}` } function Process82(input: string) -> string { client GPT4 - prompt "Process input 82: {{ input }}" + prompt `Process input 82: ${input}` } function Process83(input: string) -> string { client GPT4 - prompt "Process input 83: {{ input }}" + prompt `Process input 83: ${input}` } function Process84(input: string) -> string { client GPT4 - prompt "Process input 84: {{ input }}" + prompt `Process input 84: ${input}` } function Process85(input: string) -> string { client GPT4 - prompt "Process input 85: {{ input }}" + prompt `Process input 85: ${input}` } function Process86(input: string) -> string { client GPT4 - prompt "Process input 86: {{ input }}" + prompt `Process input 86: ${input}` } function Process87(input: string) -> string { client GPT4 - prompt "Process input 87: {{ input }}" + prompt `Process input 87: ${input}` } function Process88(input: string) -> string { client GPT4 - prompt "Process input 88: {{ input }}" + prompt `Process input 88: ${input}` } function Process89(input: string) -> string { client GPT4 - prompt "Process input 89: {{ input }}" + prompt `Process input 89: ${input}` } function Process90(input: string) -> string { client GPT4 - prompt "Process input 90: {{ input }}" + prompt `Process input 90: ${input}` } function Process91(input: string) -> string { client GPT4 - prompt "Process input 91: {{ input }}" + prompt `Process input 91: ${input}` } function Process92(input: string) -> string { client GPT4 - prompt "Process input 92: {{ input }}" + prompt `Process input 92: ${input}` } function Process93(input: string) -> string { client GPT4 - prompt "Process input 93: {{ input }}" + prompt `Process input 93: ${input}` } function Process94(input: string) -> string { client GPT4 - prompt "Process input 94: {{ input }}" + prompt `Process input 94: ${input}` } function Process95(input: string) -> string { client GPT4 - prompt "Process input 95: {{ input }}" + prompt `Process input 95: ${input}` } function Process96(input: string) -> string { client GPT4 - prompt "Process input 96: {{ input }}" + prompt `Process input 96: ${input}` } function Process97(input: string) -> string { client GPT4 - prompt "Process input 97: {{ input }}" + prompt `Process input 97: ${input}` } function Process98(input: string) -> string { client GPT4 - prompt "Process input 98: {{ input }}" + prompt `Process input 98: ${input}` } function Process99(input: string) -> string { client GPT4 - prompt "Process input 99: {{ input }}" + prompt `Process input 99: ${input}` } function Process100(input: string) -> string { client GPT4 - prompt "Process input 100: {{ input }}" + prompt `Process input 100: ${input}` } diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/header_in_llm_function/baml_tests__broken_syntax__header_in_llm_function__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/header_in_llm_function/baml_tests__broken_syntax__header_in_llm_function__05_diagnostics.snap index 9b4b402affd..32cdb6210ec 100644 --- a/baml_language/crates/baml_tests/snapshots/broken_syntax/header_in_llm_function/baml_tests__broken_syntax__header_in_llm_function__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/broken_syntax/header_in_llm_function/baml_tests__broken_syntax__header_in_llm_function__05_diagnostics.snap @@ -52,22 +52,6 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── expected `identifier`, found `'}'` ╰──── - [hir] E0010 - - × LLM function `FetchDatax` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[test.baml:12:44] - 12 │ ╭─▶ function FetchDatax(user_id: string) -> Hi { - 13 │ │ //# collect source material - 14 │ │ client VertexGCP - 15 │ │ - 16 │ │ prompt #" - 17 │ │ give short story {{ user_id }} - 18 │ │ {{ ctx.output_format }} - 19 │ │ "# - 20 │ ├─▶ } - · ╰──── migrate to a backtick prompt - ╰──── - [parse] E0010 × invalid syntax @@ -76,3 +60,11 @@ source: crates/baml_tests/src/generated_tests.rs · ─────────────┬───────────── · ╰── header comments (`//#`) are not allowed inside LLM functions ╰──── + + [type] E0003 + + × unresolved name: `VertexGCP` + ╭─[test.baml:14:12] + 14 │ client VertexGCP + · ───────── + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_speculative/baml_tests__broken_syntax__parser_speculative__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_speculative/baml_tests__broken_syntax__parser_speculative__05_diagnostics.snap index 88d2f1eb5d1..439929ebba9 100644 --- a/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_speculative/baml_tests__broken_syntax__parser_speculative__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_speculative/baml_tests__broken_syntax__parser_speculative__05_diagnostics.snap @@ -11,17 +11,6 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── expected `LLM function missing 'prompt' field`, found `'}'` ╰──── - [hir] E0010 - - × LLM function `AnotherLLM` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[ambiguous_function.baml:8:46] - 8 │ ╭─▶ function AnotherLLM(input: string) -> string { - 9 │ │ clent GPT4 // Typo in "client" - 10 │ │ prompt #"Process: {{ input }}"# // "prompt" keyword helps disambiguate - 11 │ ├─▶ } - · ╰──── migrate to a backtick prompt - ╰──── - [parse] E0010 × unexpected token @@ -49,48 +38,26 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── expected `LLM function missing 'client' field`, found `'}'` ╰──── - [hir] E0010 + [type] E0003 - × LLM function `ExtractData` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[llm_function.baml:1:48] - 1 │ ╭─▶ function ExtractData(text: string) -> JsonData { - 2 │ │ client GPT4 - 3 │ │ prompt #" - 4 │ │ Extract structured data from: {{ text }} - 5 │ │ Return as JSON. - 6 │ │ "# - 7 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `GPT4` + ╭─[llm_function.baml:2:10] + 2 │ client GPT4 + · ──── ╰──── - [hir] E0010 + [type] E0003 - × LLM function `LLMAnalyze` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[mixed_functions.baml:2:47] - 2 │ ╭─▶ function LLMAnalyze(text: string) -> Analysis { - 3 │ │ client GPT4 - 4 │ │ prompt #" - 5 │ │ Analyze the following text: - 6 │ │ {{ text }} - 7 │ │ - 8 │ │ Return an Analysis object with sentiment and keywords. - 9 │ │ "# - 10 │ ├─▶ } - · ╰──── migrate to a backtick prompt - ╰──── + × unresolved name: `GPT4` + ╭─[mixed_functions.baml:3:10] + 3 │ client GPT4 + · ──── + ╰──── - [hir] E0010 + [type] E0003 - × LLM function `ChainedProcess` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[mixed_functions.baml:31:50] - 31 │ ╭─▶ function ChainedProcess(input: string) -> string { - 32 │ │ client GPT4 - 33 │ │ prompt #" - 34 │ │ Process this through multiple steps: - 35 │ │ 1. {{ input }} - 36 │ │ 2. Transform it - 37 │ │ 3. Return result - 38 │ │ "# - 39 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `GPT4` + ╭─[mixed_functions.baml:32:10] + 32 │ client GPT4 + · ──── ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_strings/baml_tests__broken_syntax__parser_strings__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_strings/baml_tests__broken_syntax__parser_strings__05_diagnostics.snap index 3c3d33dedb2..05a0e3b645f 100644 --- a/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_strings/baml_tests__broken_syntax__parser_strings__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/broken_syntax/parser_strings/baml_tests__broken_syntax__parser_strings__05_diagnostics.snap @@ -217,34 +217,20 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── expected `Unexpected token in class body`, found `')'` ╰──── - [hir] E0010 + [type] E0003 - × LLM function `FormatMessage` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[nested_quotes.baml:11:48] - 11 │ ╭─▶ function FormatMessage(name: string) -> string { - 12 │ │ client GPT4 - 13 │ │ prompt #" - 14 │ │ Generate a message for "{{ name }}" - 15 │ │ Include quotes like: "Welcome, '{{ name }}'!" - 16 │ │ "# - 17 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `GPT4` + ╭─[nested_quotes.baml:12:10] + 12 │ client GPT4 + · ──── ╰──── - [hir] E0010 + [type] E0003 - × LLM function `ProcessText` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[raw_strings.baml:13:47] - 13 │ ╭─▶ function ProcessText(input: string) -> string { - 14 │ │ client GPT4 - 15 │ │ prompt #" - 16 │ │ Process the following text: - 17 │ │ {{ input }} - 18 │ │ - 19 │ │ Return the processed result. - 20 │ │ "# - 21 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `GPT4` + ╭─[raw_strings.baml:14:10] + 14 │ client GPT4 + · ──── ╰──── [hir] E0107 @@ -350,7 +336,7 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:27:3] - 27 │ prompt #" + 27 │ prompt ` · ───┬── · ╰── expected `config block`, found `identifier` ╰──── @@ -359,25 +345,16 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:27:10] - 27 │ prompt #" + 27 │ prompt ` · ┬ - · ╰── expected `top-level declaration`, found `'#'` - ╰──── - - [parse] E0010 - - × unexpected token - ╭─[unicode_strings.baml:27:11] - 27 │ prompt #" - · ┬ - · ╰── expected `top-level declaration`, found `'"'` + · ╰── expected `top-level declaration`, found `'`'` ╰──── [parse] E0010 × unexpected token ╭─[unicode_strings.baml:28:5] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ───┬─── · ╰── expected `top-level declaration`, found `identifier` ╰──── @@ -386,7 +363,7 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:28:13] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ─┬─ · ╰── expected `top-level declaration`, found `identifier` ╰──── @@ -395,7 +372,7 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:28:17] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ──┬─ · ╰── expected `top-level declaration`, found `identifier` ╰──── @@ -404,7 +381,7 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:28:21] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ┬ · ╰── expected `top-level declaration`, found `':'` ╰──── @@ -413,16 +390,16 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:28:23] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ┬ - · ╰── expected `top-level declaration`, found `'{'` + · ╰── expected `top-level declaration`, found `'$'` ╰──── [parse] E0010 × unexpected token ╭─[unicode_strings.baml:28:24] - 28 │ Process the text: {{ 输入 }} + 28 │ Process the text: ${输入} · ┬ · ╰── expected `top-level declaration`, found `'{'` ╰──── @@ -430,37 +407,28 @@ source: crates/baml_tests/src/generated_tests.rs [parse] E0010 × unexpected token - ╭─[unicode_strings.baml:28:26] - 28 │ Process the text: {{ 输入 }} - · ─┬ - · ╰── expected `top-level declaration`, found `error` - ╰──── - - [parse] E0010 - - × unexpected token - ╭─[unicode_strings.baml:28:27] - 28 │ Process the text: {{ 输入 }} - · ─┬ - · ╰── expected `top-level declaration`, found `error` + ╭─[unicode_strings.baml:28:25] + 28 │ Process the text: ${输入} + · ─┬ + · ╰── expected `top-level declaration`, found `error` ╰──── [parse] E0010 × unexpected token - ╭─[unicode_strings.baml:28:29] - 28 │ Process the text: {{ 输入 }} - · ┬ - · ╰── expected `top-level declaration`, found `'}'` + ╭─[unicode_strings.baml:28:26] + 28 │ Process the text: ${输入} + · ─┬ + · ╰── expected `top-level declaration`, found `error` ╰──── [parse] E0010 × unexpected token - ╭─[unicode_strings.baml:28:30] - 28 │ Process the text: {{ 输入 }} - · ┬ - · ╰── expected `top-level declaration`, found `'}'` + ╭─[unicode_strings.baml:28:27] + 28 │ Process the text: ${输入} + · ┬ + · ╰── expected `top-level declaration`, found `'}'` ╰──── [parse] E0010 @@ -539,18 +507,9 @@ source: crates/baml_tests/src/generated_tests.rs × unexpected token ╭─[unicode_strings.baml:30:3] - 30 │ "# + 30 │ ` · ┬ - · ╰── expected `top-level declaration`, found `'"'` - ╰──── - - [parse] E0010 - - × unexpected token - ╭─[unicode_strings.baml:30:4] - 30 │ "# - · ┬ - · ╰── expected `top-level declaration`, found `'#'` + · ╰── expected `top-level declaration`, found `'`'` ╰──── [parse] E0010 diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/pending_greaters_fix/baml_tests__broken_syntax__pending_greaters_fix__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/pending_greaters_fix/baml_tests__broken_syntax__pending_greaters_fix__05_diagnostics.snap index de58357140b..e921a9cf445 100644 --- a/baml_language/crates/baml_tests/snapshots/broken_syntax/pending_greaters_fix/baml_tests__broken_syntax__pending_greaters_fix__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/broken_syntax/pending_greaters_fix/baml_tests__broken_syntax__pending_greaters_fix__05_diagnostics.snap @@ -20,15 +20,12 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── unmatched `>` in type expression (found 1 extra) ╰──── - [hir] E0010 - - × LLM function `BadParam` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[extra_greater.baml:23:51] - 23 │ ╭─▶ function BadParam(x: map>) -> string { - 24 │ │ client GPT4 - 25 │ │ prompt #"test"# - 26 │ ├─▶ } - · ╰──── migrate to a backtick prompt + [type] E0003 + + × unresolved name: `GPT4` + ╭─[extra_greater.baml:24:10] + 24 │ client GPT4 + · ──── ╰──── [parse] E0010 @@ -40,15 +37,12 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── unmatched `>` in type expression (found 1 extra) ╰──── - [hir] E0010 + [type] E0003 - × LLM function `BadReturnType` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[extra_greater.baml:29:47] - 29 │ ╭─▶ function BadReturnType() -> map> { - 30 │ │ client GPT4 - 31 │ │ prompt #"test"# - 32 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `GPT4` + ╭─[extra_greater.baml:30:10] + 30 │ client GPT4 + · ──── ╰──── [parse] E0010 diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap new file mode 100644 index 00000000000..837244ea753 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== COMPILER2 DIAGNOSTICS === + [parse] E0010 + + × invalid syntax + ╭─[main.baml:1:1] + 1 │ template_string Greeting(name: string) `Hello ${name}` + · ───────┬─────── + · ╰── `template_string` is no longer supported. Use a function returning a backtick string instead. + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__03_ppir.snap index eaeea68965a..d5c349ddfde 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__03_ppir.snap @@ -17,6 +17,9 @@ function user.Greet$spec(name: string) -> ai.FunctionSpec [expr] { function user.Greet$stream(name: string, client: ai.stream.StreamingClient? = null) -> ai.stream.Stream [expr] { ai.stream.from_spec(Greet$spec(name), client = client) } +function user.Header(title: string) -> string [expr] { + { } `...` +} --- captures --- lambda ( __spec_output_format) in Greet$spec: captures [name] diff --git a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_5_mir.snap b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_5_mir.snap index c85764dd1f3..b5302d2cf7a 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_5_mir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_5_mir.snap @@ -229,3 +229,25 @@ fn user.Greet$stream(name: string, client: null | ai.stream.StreamingClient) -> return; } } + +fn user.Header(title: string) -> string { + // Locals: + let _0: string // _0 // return + let _1: string // title // param + let _2: string + let _3: type + + bb0: { + _3 = load_type(string); + _2 = call const fn baml.String.from(copy _1) -> [bb1]; + } + + bb1: { + _0 = const "# " + copy _2; + goto -> bb2; + } + + bb2: { + return; + } +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_tir.snap b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_tir.snap index 2c0cf645a00..ed8c0d156be 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__04_tir.snap @@ -16,6 +16,11 @@ function user.Greet$render_prompt(name: string) -> ai.Prompt throws never { function user.Greet$parse(json: string) -> string throws never { baml.sap.parse(json) : string } +function user.Header(title: string) -> string throws never { + { : string + `...` : string + } +} function user.Greet$stream(name: string, client: ai.stream.StreamingClient | null = null) -> ai.stream.Stream throws never { ai.stream.from_spec(Greet$spec(name), client = client) : ai.stream.Stream } diff --git a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__06_codegen.snap b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__06_codegen.snap index 18d57440002..1e6181baa66 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__06_codegen.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__06_codegen.snap @@ -98,3 +98,14 @@ function user.Greet$stream(name: string, client: ai.stream.StreamingClient | nul call ai.stream.from_spec return } + +function user.Header(title: string) -> string { + load_type string + load_var title + call baml.String.from + store_var _2 + load_const "# " + load_var _2 + bin_op + + return +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__10_formatter__main.snap b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__10_formatter__main.snap index 51d304165f3..96180015afc 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__10_formatter__main.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__10_formatter__main.snap @@ -1,9 +1,8 @@ --- source: crates/baml_tests/src/generated_tests.rs --- -// BEP-049 backtick string literals in the declarative slots that the formatter -// used to reject (it only knew the quoted and raw string tokens there). Both -// slots below compile cleanly and format without error. +// BEP-049 backtick string literals in prompt and expression positions compile +// cleanly and format without error. // // `baml fmt` re-indents a multi-line backtick interior to sit one level past the // surrounding block (like a raw string), but only when that preserves the @@ -23,7 +22,9 @@ function Greet(name: string) -> string { ` } -// Backtick body in a `template_string`. -template_string Header(title: string) ` - # ${title} -` +// Backtick value in expression position. +function Header(title: string) -> string { + ` + # ${title} + ` +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__03_ppir.snap index 1e012669a07..f6093d79f68 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__03_ppir.snap @@ -1,5 +1,16 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 30086 --- === PPIR === +function user.SplitCount(s: string) -> string [expr] { + { } `...` +} +function user.StartsWith(s: string) -> string [expr] { + { } `...` +} +function user.Trimmed(s: string) -> string [expr] { + { } `...` +} +function user.Upper(s: string) -> string [expr] { + { } `...` +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_5_mir.snap b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_5_mir.snap index 4eabbd3e622..28154194efc 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_5_mir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_5_mir.snap @@ -2,3 +2,92 @@ source: crates/baml_tests/src/generated_tests.rs --- === MIR2 === +fn user.Upper(s: string) -> string { + // Locals: + let _0: string // _0 // return + let _1: string // s // param + let _2: string + let _3: type + + bb0: { + _2 = call const fn baml.String.to_upper_case(copy _1) -> [bb1]; + } + + bb1: { + _3 = load_type(string); + _0 = call const fn baml.String.from(copy _2) -> [bb2]; + } + + bb2: { + return; + } +} + +fn user.StartsWith(s: string) -> string { + // Locals: + let _0: string // _0 // return + let _1: string // s // param + let _2: bool + let _3: type + + bb0: { + _2 = call const fn baml.String.starts_with(copy _1, const "hello") -> [bb1]; + } + + bb1: { + _3 = load_type(bool); + _0 = call const fn baml.String.from(copy _2) -> [bb2]; + } + + bb2: { + return; + } +} + +fn user.SplitCount(s: string) -> string { + // Locals: + let _0: string // _0 // return + let _1: string // s // param + let _2: int + let _3: string[] + let _4: type + + bb0: { + _3 = call const fn baml.String.split(copy _1, const ",") -> [bb1]; + } + + bb1: { + _2 = len(_3); + goto -> bb2; + } + + bb2: { + _4 = load_type(int); + _0 = call const fn baml.String.from(copy _2) -> [bb3]; + } + + bb3: { + return; + } +} + +fn user.Trimmed(s: string) -> string { + // Locals: + let _0: string // _0 // return + let _1: string // s // param + let _2: string + let _3: type + + bb0: { + _2 = call const fn baml.String.trim(copy _1) -> [bb1]; + } + + bb1: { + _3 = load_type(string); + _0 = call const fn baml.String.from(copy _2) -> [bb2]; + } + + bb2: { + return; + } +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_tir.snap b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_tir.snap index da37c0436f4..2e391cb4afe 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__04_tir.snap @@ -2,3 +2,23 @@ source: crates/baml_tests/src/generated_tests.rs --- === TIR2 === +function user.Upper(s: string) -> string throws never { + { : string + `...` : string + } +} +function user.StartsWith(s: string) -> string throws never { + { : string + `...` : string + } +} +function user.SplitCount(s: string) -> string throws never { + { : string + `...` : string + } +} +function user.Trimmed(s: string) -> string throws never { + { : string + `...` : string + } +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__06_codegen.snap b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__06_codegen.snap index cb7132e9ee8..37cdcd74046 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__06_codegen.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__06_codegen.snap @@ -1,4 +1,45 @@ --- source: crates/baml_tests/src/generated_tests.rs --- +function user.SplitCount(s: string) -> string { + load_var s + load_const "," + call baml.String.split + container_len + store_var _2 + load_type int + load_var _2 + call baml.String.from + return +} +function user.StartsWith(s: string) -> string { + load_var s + load_const "hello" + call baml.String.starts_with + store_var _2 + load_type bool + load_var _2 + call baml.String.from + return +} + +function user.Trimmed(s: string) -> string { + load_var s + call baml.String.trim + store_var _2 + load_type string + load_var _2 + call baml.String.from + return +} + +function user.Upper(s: string) -> string { + load_var s + call baml.String.to_upper_case + store_var _2 + load_type string + load_var _2 + call baml.String.from + return +} diff --git a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__10_formatter__string_methods.snap b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__10_formatter__string_methods.snap index b0ad002fef5..01a9c67c5d2 100644 --- a/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__10_formatter__string_methods.snap +++ b/baml_language/crates/baml_tests/snapshots/compiles/string_methods/baml_tests__compiles__string_methods__10_formatter__string_methods.snap @@ -1,14 +1,20 @@ --- source: crates/baml_tests/src/generated_tests.rs --- -// Test: string method calls inside backtick template holes produce no -// diagnostics. (Jinja template_string bodies went away with the legacy LLM -// path; templates are backticks with `${...}` holes now.) +// Test: string method calls inside backtick interpolation produce no diagnostics. -template_string Upper(s: string) `${s.to_upper_case()}` +function Upper(s: string) -> string { + `${s.to_upper_case()}` +} -template_string StartsWith(s: string) `${s.starts_with("hello")}` +function StartsWith(s: string) -> string { + `${s.starts_with("hello")}` +} -template_string SplitCount(s: string) `${s.split(",").length()}` +function SplitCount(s: string) -> string { + `${s.split(",").length()}` +} -template_string Trimmed(s: string) `${s.trim()}` +function Trimmed(s: string) -> string { + `${s.trim()}` +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__03_ppir.snap index 1720fb2a021..42db2000237 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__03_ppir.snap @@ -2,7 +2,6 @@ source: crates/baml_tests/src/generated_tests.rs --- === PPIR === -function user.LegacyJinjaPrompt(input: string, client: ai.Client? = null) -> string [llm] function user.LegacyRoleMarker(input: string, client: ai.Client? = null) -> string [llm] { ai.Agent.new(client = client).run(LegacyRoleMarker$spec(input)).value } @@ -18,6 +17,21 @@ function user.LegacyRoleMarker$spec(input: string) -> ai.FunctionSpec [ function user.LegacyRoleMarker$stream(input: string, client: ai.stream.StreamingClient? = null) -> ai.stream.Stream [expr] { ai.stream.from_spec(LegacyRoleMarker$spec(input), client = client) } +function user.MigratedPrompt(input: string, client: ai.Client? = null) -> string [llm] { + ai.Agent.new(client = client).run(MigratedPrompt$spec(input)).value +} +function user.MigratedPrompt$parse(json: string) -> string [expr] { + baml.sap.parse(json) +} +function user.MigratedPrompt$render_prompt(input: string) -> ai.Prompt [expr] { + MigratedPrompt$spec(input).prompt(output_format = ai.wire.render_output_format(reflect.type_of())) +} +function user.MigratedPrompt$spec(input: string) -> ai.FunctionSpec [expr] { + ai.FunctionSpec { spec_name: "MigratedPrompt", args: map { "input": input }, prompt_template: ( __spec_output_format: string) -> { { let ctx = ai.internal.SpecCtx { output_format: __spec_output_format }; let __spec_tagged_prompt = { let __tt_parts: string[] = []; let __tt_values: unknown[] = []; let __tt_cur = ""; __tt_cur = __tt_cur Add "Echo: "; __tt_parts.push( __tt_cur); __tt_cur = ""; __tt_values.push({ } input); __tt_parts.push( __tt_cur) } baml.TaggedString { parts: __tt_parts, values: __tt_values } } ai.internal.assemble_prompt( __spec_tagged_prompt.parts, __spec_tagged_prompt.values) }, toolbox: ai.tools.Toolbox.new([]), default_client: ValidClient } +} +function user.MigratedPrompt$stream(input: string, client: ai.stream.StreamingClient? = null) -> ai.stream.Stream [expr] { + ai.stream.from_spec(MigratedPrompt$spec(input), client = client) +} function user.UnknownProviderPrefix(input: string, client: ai.Client? = null) -> string [llm] function user.ValidFunction(input: string, client: ai.Client? = null) -> string [llm] { ai.Agent.new(client = client).run(ValidFunction$spec(input)).value @@ -36,5 +50,6 @@ function user.ValidFunction$stream(input: string, client: ai.stream.StreamingCli } --- captures --- +lambda ( __spec_output_format) in MigratedPrompt$spec: captures [input] lambda ( __spec_output_format) in LegacyRoleMarker$spec: captures [input] lambda ( __spec_output_format) in ValidFunction$spec: captures [input] diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__04_tir.snap index 445a356d4a3..1814b27eea1 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__04_tir.snap @@ -2,6 +2,20 @@ source: crates/baml_tests/src/generated_tests.rs --- === TIR2 === +function user.MigratedPrompt(input: string, client: ai.Client | null = null) -> string throws never { + ai.Agent.new(client = client).run(MigratedPrompt$spec(input)).value : string +} +function user.MigratedPrompt$spec(input: string) -> ai.FunctionSpec throws never { + ai.FunctionSpec { spec_name: "MigratedPrompt", args: map { "input": input }, prompt_template: ( __spec_output_format: string) -> { ... }, toolbox: ai.tools.Toolbox.new([]), default_client: ValidClient } : ai.FunctionSpec +} +lambda user.MigratedPrompt$spec { +} +function user.MigratedPrompt$render_prompt(input: string) -> ai.Prompt throws never { + MigratedPrompt$spec(input).prompt(output_format = ai.wire.render_output_format(reflect.type_of())) : ai.Prompt +} +function user.MigratedPrompt$parse(json: string) -> string throws never { + baml.sap.parse(json) : string +} function user.LegacyRoleMarker(input: string, client: ai.Client | null = null) -> string throws never { ai.Agent.new(client = client).run(LegacyRoleMarker$spec(input)).value : string } @@ -30,6 +44,9 @@ function user.ValidFunction$render_prompt(input: string) -> ai.Prompt throws nev function user.ValidFunction$parse(json: string) -> string throws never { baml.sap.parse(json) : string } +function user.MigratedPrompt$stream(input: string, client: ai.stream.StreamingClient | null = null) -> ai.stream.Stream throws never { + ai.stream.from_spec(MigratedPrompt$spec(input), client = client) : ai.stream.Stream +} function user.LegacyRoleMarker$stream(input: string, client: ai.stream.StreamingClient | null = null) -> ai.stream.Stream throws never { ai.stream.from_spec(LegacyRoleMarker$spec(input), client = client) : ai.stream.Stream } diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__05_diagnostics.snap index b9fa78b4700..9ec0f5f2548 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__05_diagnostics.snap @@ -31,17 +31,6 @@ source: crates/baml_tests/src/generated_tests.rs [hir] E0010 - × LLM function `LegacyJinjaPrompt` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[client_option_types.baml:25:53] - 25 │ ╭─▶ function LegacyJinjaPrompt(input: string) -> string { - 26 │ │ client ValidClient - 27 │ │ prompt #"Echo: {{ input }}"# - 28 │ ├─▶ } - · ╰──── migrate to a backtick prompt - ╰──── - - [hir] E0010 - × LLM function `UnknownProviderPrefix` has an unusable `client`: no builtin provider for prefix "aws-bedrock"; construct a client value instead (OpenAI-compatible endpoints: │ openai.OpenAiClient.new(base_url = ..., model = "anthropic.claude-3-haiku")) ╭─[client_option_types.baml:38:57] diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__10_formatter__client_option_types.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__10_formatter__client_option_types.snap index 41f72777df5..d5219bf233d 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__10_formatter__client_option_types.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/client_option_types/baml_tests__diagnostic_errors__client_option_types__10_formatter__client_option_types.snap @@ -24,14 +24,14 @@ retry_policy LegacyRetry { max_retries: 3, } -// Removed: Jinja `#"..."#` prompt on an LLM function. -function LegacyJinjaPrompt(input: string) -> string { +// Jinja prompt migrated to a backtick prompt. +function MigratedPrompt(input: string) -> string { client: ValidClient - prompt: #"Echo: {{ input }}"# + prompt: `Echo: ${input}` } // Structural role markers remain valid while the surrounding fixture checks -// the independently removed client/Jinja forms. +// the independently removed client forms. function LegacyRoleMarker(input: string) -> string { client: ValidClient prompt: `${role("system")}Be terse. ${input}` diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__03_ppir.snap index 1dab97da255..d91f18615c1 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__03_ppir.snap @@ -435,7 +435,21 @@ function user.greet() -> string [expr] { function user.processData(first_input: map, second_input: int | string | bool[], third_input: string) -> map [expr] { { } map { "result": 1 } } -function user.TestTarget(client: ai.Client? = null) -> string [llm] +function user.TestTarget(client: ai.Client? = null) -> string [llm] { + ai.Agent.new(client = client).run(TestTarget$spec()).value +} +function user.TestTarget$parse(json: string) -> string [expr] { + baml.sap.parse(json) +} +function user.TestTarget$render_prompt() -> ai.Prompt [expr] { + TestTarget$spec().prompt(output_format = ai.wire.render_output_format(reflect.type_of())) +} +function user.TestTarget$spec() -> ai.FunctionSpec [expr] { + ai.FunctionSpec { spec_name: "TestTarget", args: map { }, prompt_template: ( __spec_output_format: string) -> { { let ctx = ai.internal.SpecCtx { output_format: __spec_output_format }; let __spec_tagged_prompt = { let __tt_parts: string[] = []; let __tt_values: unknown[] = []; let __tt_cur = ""; __tt_cur = __tt_cur Add "Hello"; __tt_parts.push( __tt_cur) } baml.TaggedString { parts: __tt_parts, values: __tt_values } } ai.internal.assemble_prompt( __spec_tagged_prompt.parts, __spec_tagged_prompt.values) }, toolbox: ai.tools.Toolbox.new([]), default_client: BasicClient } +} +function user.TestTarget$stream(client: ai.stream.StreamingClient? = null) -> ai.stream.Stream [expr] { + ai.stream.from_spec(TestTarget$spec(), client = client) +} enum user.DottedAttrEnum {X, Y} enum user.DynamicEnum {X, Y} enum user.E {V} @@ -710,6 +724,42 @@ class user.OtherVec$stream { function user.OtherExprs(a: int, b: int) -> int [expr] { { 0; 1; 42; 999999; 0.0; 3.14; 99.999; "hello"; "a short string"; "a much longer str..."; true; false; null; "raw string"; "raw string with \"..."; "\n multi-li..."; a; a Add b; a; a Add b Mul a Sub b; a Add b Mul a Sub b Add a Mul a Add b Mul b Mul a Add b Mul a Add b Sub a Sub b Mul a Sub b; a; a; Neg a; Not true; Neg Neg a; Not a Gt b; Neg a; Neg a; []; [1]; [1, 2, 3]; [1, 2, 3, 4, 5]; ["short", "array"]; ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q"]; ["this is a long st...", "another long stri...", "yet another one t...", "and one more stri..."]; ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliet", "kilo", "lima", "mike", "november", "oscar", "papa"]; [[1, 2], [3, 4]]; [[1, 2, 3], [4, 5, 6], [7, 8, 9]]; [[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]], [[13, 14], [15, 16]]]; [1, 2]; [1, 2]; ["this is a long st...", "another long stri...", "yet another that ...", "and one more stri..."]; ["this is a long st...", "another long stri...", "yet another that ...", "and one more stri..."]; map { "key": 1 }; map { "a": 1, "b": 2 }; map { "a": 1, "b": 2, "c": 3, "d": 4, "e": 5 }; map { "first_key": "first value", "second_key": "second value", "third_key": "third value", "fourth_key": "fourth value", "fifth_key": "fifth" }; map { "outer": map { "inner_a": 1, "inner_b": 2, "inner_c": 3 }, "other": map { "inner_d": 4, "inner_e": 5, "inner_f": 6 } }; map { "k": 1 }; map { "k": 1 }; user.OtherPoint { x: 1, y: 2 }; user.OtherVec { p: user.OtherPoint { x: 1, y: 2 }, q: user.OtherPoint { x: 3, y: 4 } }; user.OtherVec { p: user.OtherPoint { x: 100000, y: 200000 }, q: user.OtherPoint { x: 300000, y: 400000 } }; user.OtherTriple { a: user.OtherVec { p: user.OtherPoint { x: 1, y: 2 }, q: user.OtherPoint { x: 3, y: 4 } }, b: user.OtherVec { p: user.OtherPoint { x: 5, y: 6 }, q: user.OtherPoint { x: 7, y: 8 } }, c: user.OtherVec { p: user.OtherPoint { x: 9, y: 10 }, q: user.OtherPoint { x: 11, y: 12 } } }; user.OtherPoint { x: 1, y: 2 }; user.OtherPoint { x: 1, y: 2 }; user.OtherVec { p: user.OtherPoint { x: 100000, y: 200000 }, q: user.OtherPoint { x: 300000, y: 400000 } }; { let x = 1 } x Add 1; { let x = a Add b; let y = x Mul 2 } y; { let x = { let y = { let z = a Add b } z Mul z } y Add y } x Mul x; { let x = if (a Gt b) { } match (a) { 0 => { let inner = b Mul 2 } inner Add 1, _ => a } else { } b } x; [1, 2, 3]; map { "k": 1 }; user.OtherPoint { x: 1, y: 2 } } a Add b } +function user.BasicTemplate(name: string) -> string [expr] { + { } `...` +} +function user.ComplexParams(items: string[], config: map) -> string [expr] { + { } `...` +} +function user.ComplexTypesWrap(items: int | string | bool[], config: map, callback: (x: int, y: int) -> string) -> string [expr] { + { } `...` +} +function user.FnTypeParams(transform: (x: int) -> int, combine: (a: string, b: string) -> string) -> string [expr] { + { } `...` +} +function user.ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) -> string [expr] { + { } `...` +} +function user.MultiParam(first: string, last: string, age: int) -> string [expr] { + { } `...` +} +function user.NoParams() -> string [expr] { + { } `...` +} +function user.OptionalParams(name: string?, age: int?, items: string[]?) -> string [expr] { + { } `...` +} +function user.SingleLine(x: int) -> string [expr] { + { } `...` +} +function user.TriviaTemplate(x: string) -> string [expr] { + { } `...` +} +function user.WithConditional(show: bool, value: string) -> string [expr] { + { } `...` +} +function user.WithControl(items: string[]) -> string [expr] { + { } `...` +} class user.TypeAliasFailure { reason: string } diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__04_tir.snap index 3712650708e..f7cd79e1d45 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__04_tir.snap @@ -576,6 +576,24 @@ class user.SemicolonTrivia$stream { name: string | null age: int | null } +function user.TestTarget(client: ai.Client | null = null) -> string throws never { + ai.Agent.new(client = client).run(TestTarget$spec()).value : string +} +function user.TestTarget$spec() -> ai.FunctionSpec throws never { + ai.FunctionSpec { spec_name: "TestTarget", args: map { }, prompt_template: ( __spec_output_format: string) -> { ... }, toolbox: ai.tools.Toolbox.new([]), default_client: BasicClient } : ai.FunctionSpec + !! 13601..13612: unresolved name: BasicClient +} +lambda user.TestTarget$spec { +} +function user.TestTarget$render_prompt() -> ai.Prompt throws never { + TestTarget$spec().prompt(output_format = ai.wire.render_output_format(reflect.type_of())) : ai.Prompt +} +function user.TestTarget$parse(json: string) -> string throws never { + baml.sap.parse(json) : string +} +function user.TestTarget$stream(client: ai.stream.StreamingClient | null = null) -> ai.stream.Stream throws never { + ai.stream.from_spec(TestTarget$spec(), client = client) : ai.stream.Stream +} enum user.Minimal enum user.TwoVariants enum user.MultiVariant @@ -1751,6 +1769,106 @@ class user.OtherTriple$stream { b: user.OtherVec$stream | null c: user.OtherVec$stream | null } +function user.BasicTemplate(name: string) -> string throws never { + { : string + `...` : string + } + !! 173..177: type mismatch: expected T, got string + !! 163..180: cannot infer type parameter `T` +} +function user.NoParams() -> string throws never { + { : "A static template with no parameters." + `...` : "A static template with no parameters." + } +} +function user.MultiParam(first: string, last: string, age: int) -> string throws never { + { : string + `...` : string + } + !! 356..361: type mismatch: expected T, got string + !! 338..396: cannot infer type parameter `T` + !! 365..369: type mismatch: expected T, got string + !! 338..396: cannot infer type parameter `T` + !! 338..396: cannot infer type parameter `T` +} +function user.ComplexParams(items: string[], config: map) -> string throws never { + { : string + `...` : string + } + !! 501..506: type mismatch: expected T, got string[] + !! 482..539: cannot infer type parameter `T` + !! 526..532: type mismatch: expected T, got map + !! 482..539: cannot infer type parameter `T` +} +function user.SingleLine(x: int) -> string throws never { + { : string + `...` : string + } + !! 583..598: cannot infer type parameter `T` +} +function user.ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) -> string throws never { + { : string + `...` : string + } + !! 738..753: type mismatch: expected T, got string + !! 735..814: cannot infer type parameter `T` + !! 757..773: type mismatch: expected T, got string + !! 735..814: cannot infer type parameter `T` + !! 735..814: cannot infer type parameter `T` + !! 796..812: type mismatch: expected T, got bool + !! 735..814: cannot infer type parameter `T` +} +function user.ComplexTypesWrap(items: (int | string | bool)[], config: map, callback: (x: int, y: int) -> string throws never) -> string throws never { + { : string + `...` : string + } + !! 968..973: type mismatch: expected T, got (int | string | bool)[] + !! 965..985: cannot infer type parameter `T` + !! 977..983: type mismatch: expected T, got map + !! 965..985: cannot infer type parameter `T` +} +function user.OptionalParams(name: string | null, age: int | null, items: string[] | null) -> string throws never { + { : string + `...` : string + } + !! 1076..1080: type mismatch: expected T, got string | null + !! 1073..1098: cannot infer type parameter `T` + !! 1084..1087: type mismatch: expected T, got int | null + !! 1073..1098: cannot infer type parameter `T` + !! 1091..1096: type mismatch: expected T, got string[] | null + !! 1073..1098: cannot infer type parameter `T` + !! 1075..1081: cannot interpolate a value of type `string | null` — it may be null; coalesce with `?? "…"` or unwrap it first + !! 1083..1088: cannot interpolate a value of type `int | null` — it may be null; coalesce with `?? "…"` or unwrap it first + !! 1090..1097: cannot interpolate a value of type `string[] | null` — it may be null; coalesce with `?? "…"` or unwrap it first +} +function user.FnTypeParams(transform: (x: int) -> int throws never, combine: (a: string, b: string) -> string throws never) -> string throws never { + { : "transformed" + `...` : "transformed" + } +} +function user.TriviaTemplate(x: string) -> string throws never { + { : string + `...` : string + } + !! 1463..1464: type mismatch: expected T, got string + !! 1454..1466: cannot infer type parameter `T` +} +function user.WithControl(items: string[]) -> string throws never { + { : string + `...` : string + } + !! 1573..1577: type mismatch: expected T, got string + !! 1524..1602: cannot infer type parameter `T` + !! 1573..1577: type mismatch: expected T, got string + !! 1524..1602: cannot infer type parameter `T` +} +function user.WithConditional(show: bool, value: string) -> string throws never { + { : string | "hidden" + `...` : string | "hidden" + } + !! 1689..1694: type mismatch: expected T, got string + !! 1674..1717: cannot infer type parameter `T` +} type user.SimpleAlias = int type user.AliasChain = user.SimpleAlias type user.Name = string diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__05_diagnostics.snap index 71d6403275d..721fbd15c67 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__05_diagnostics.snap @@ -844,15 +844,12 @@ source: crates/baml_tests/src/generated_tests.rs · ╰──── retry composes at the client boundary now ╰──── - [hir] E0010 + [type] E0003 - × LLM function `TestTarget` uses a Jinja `#"..."#` prompt, which is removed; write a backtick template: prompt `... ${ctx.output_format}` - ╭─[config_decls.baml:445:33] - 445 │ ╭─▶ function TestTarget() -> string { - 446 │ │ client BasicClient - 447 │ │ prompt #"Hello"# - 448 │ ├─▶ } - · ╰──── migrate to a backtick prompt + × unresolved name: `BasicClient` + ╭─[config_decls.baml:446:12] + 446 │ client BasicClient + · ─────────── ╰──── [hir] E0010 @@ -931,7 +928,7 @@ source: crates/baml_tests/src/generated_tests.rs [validation] E0011 - × name `ExtraWhitespace` defined 5 times as: class, enum, function, template_string, type + × name `ExtraWhitespace` defined 4 times as: class, enum, function, type ╭─[enum_decls.baml:211:9] 211 │ enum ExtraWhitespace { · ───────┬─────── @@ -947,11 +944,6 @@ source: crates/baml_tests/src/generated_tests.rs │ 497 │ function ExtraWhitespace ( a : int , b : int ) -> int { │ · ─────────────── │ ╰──── - ├─▶ ☞ duplicate template_string definition - │ ╭─[template_string_decls.baml:364:20] - │ 364 │ template_string ExtraWhitespace ( a : string , b : int ) #" - │ · ─────────────── - │ ╰──── ╰─▶ ☞ duplicate type definition ╭─[type_alias_decls.baml:401:9] 401 │ type ExtraWhitespace = int | string | bool | float | null @@ -1132,13 +1124,114 @@ source: crates/baml_tests/src/generated_tests.rs · ╰── expected `string`, found `0` ╰──── + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:5:5] + 5 │ `Hello, ${name}!` + · ───────────────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:5:15] + 5 │ `Hello, ${name}!` + · ──┬─ + · ╰── expected `T`, found `string` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:13:5] + 13 │ ╭─▶ ` + 14 │ │ Name: ${first} ${last} + 15 │ │ Age: ${age} + 16 │ ╰─▶ ` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:13:5] + 13 │ ╭─▶ ` + 14 │ │ Name: ${first} ${last} + 15 │ │ Age: ${age} + 16 │ ╰─▶ ` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:14:17] + 14 │ Name: ${first} ${last} + · ──┬── + · ╰── expected `T`, found `string` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:14:26] + 14 │ Name: ${first} ${last} + · ──┬─ + · ╰── expected `T`, found `string` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:20:5] + 20 │ ╭─▶ ` + 21 │ │ Items: ${items} + 22 │ │ Config: ${config} + 23 │ ╰─▶ ` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:20:5] + 20 │ ╭─▶ ` + 21 │ │ Items: ${items} + 22 │ │ Config: ${config} + 23 │ ╰─▶ ` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:21:18] + 21 │ Items: ${items} + · ──┬── + · ╰── expected `T`, found `string[]` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:22:19] + 22 │ Config: ${config} + · ───┬── + · ╰── expected `T`, found `map` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:26:41] + 26 │ function SingleLine(x: int) -> string { `Value is ${x}` } + · ─────────────── + ╰──── + [validation] E0011 - × name `ManyParams` defined 2 times as: function, template_string - ╭─[template_string_decls.baml:30:17] - 30 │ template_string ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) #" - · ─────┬──── - · ╰── duplicate template_string definition + × duplicate function `ManyParams` + ╭─[template_string_decls.baml:28:10] + 28 │ function ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) -> string { + · ─────┬──── + · ╰── duplicate function definition ╰──── ╰─▶ ☞ first defined as function here ╭─[function_decls.baml:23:10] @@ -1146,59 +1239,236 @@ source: crates/baml_tests/src/generated_tests.rs · ────────── ╰──── - [validation] E0011 + [type] E0002 - × name `MultiParamTrivia` defined 2 times as: function, template_string - ╭─[template_string_decls.baml:89:17] - 89 │ template_string MultiParamTrivia( - · ────────┬─────── - · ╰── duplicate template_string definition + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:29:5] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ─────────────────────────────────────────────────────────────────────────────── ╰──── - ╰─▶ ☞ first defined as function here - ╭─[function_decls.baml:135:10] - 135 │ function MultiParamTrivia( - · ──────────────── - ╰──── - [validation] E0011 + [type] E0002 - × name `MultiParamTriviaBlock` defined 2 times as: function, template_string - ╭─[template_string_decls.baml:106:17] - 106 │ template_string MultiParamTriviaBlock( - · ──────────┬────────── - · ╰── duplicate template_string definition - ╰──── - ╰─▶ ☞ first defined as function here - ╭─[function_decls.baml:152:10] - 152 │ function MultiParamTriviaBlock( - · ───────────────────── - ╰──── + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:29:5] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ─────────────────────────────────────────────────────────────────────────────── + ╰──── - [validation] E0011 + [type] E0002 - × name `SixLongParams` defined 2 times as: function, template_string - ╭─[template_string_decls.baml:171:17] - 171 │ template_string SixLongParams(first_param: string, second_param: int, third_param: bool, fourth_param: float, fifth_param: string[], sixth_param: map) #" - · ──────┬────── - · ╰── duplicate template_string definition - ╰──── - ╰─▶ ☞ first defined as function here - ╭─[function_decls.baml:37:10] - 37 │ function SixLongParams(first_parameter_name: map, second_parameter_name: (int | string)[], third_parameter_name: map, fourth_parameter_name: int, - fifth_parameter_name: string, sixth_parameter_name: float) -> string { - · ───────────── - ╰──── + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:29:5] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ─────────────────────────────────────────────────────────────────────────────── + ╰──── - [validation] E0011 + [type] E0001 - × name `T` defined 2 times as: template_string, type - ╭─[type_alias_decls.baml:398:6] - 398 │ type T = int - · ┬ - · ╰── duplicate type definition - ╰──── - ╰─▶ ☞ first defined as template_string here - ╭─[template_string_decls.baml:123:17] - 123 │ template_string T(x: int) #"{{ x }}"# - · ─ - ╰──── + × mismatched types + ╭─[template_string_decls.baml:29:8] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ───────┬─────── + · ╰── expected `T`, found `string` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:29:27] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ────────┬─────── + · ╰── expected `T`, found `string` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:29:66] + 29 │ `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` + · ────────┬─────── + · ╰── expected `T`, found `bool` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:33:5] + 33 │ `${items} ${config}` + · ──────────────────── + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:33:5] + 33 │ `${items} ${config}` + · ──────────────────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:33:8] + 33 │ `${items} ${config}` + · ──┬── + · ╰── expected `T`, found `(int | string | bool)[]` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:33:17] + 33 │ `${items} ${config}` + · ───┬── + · ╰── expected `T`, found `map` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:37:5] + 37 │ `${name} ${age} ${items}` + · ───────────────────────── + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:37:5] + 37 │ `${name} ${age} ${items}` + · ───────────────────────── + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:37:5] + 37 │ `${name} ${age} ${items}` + · ───────────────────────── + ╰──── + + [type] E0001 + + × cannot interpolate a value of type `string | null` — it may be null; coalesce with `?? "…"` or unwrap it first + ╭─[template_string_decls.baml:37:7] + 37 │ `${name} ${age} ${items}` + · ────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:37:8] + 37 │ `${name} ${age} ${items}` + · ──┬─ + · ╰── expected `T`, found `string | null` + ╰──── + + [type] E0001 + + × cannot interpolate a value of type `int | null` — it may be null; coalesce with `?? "…"` or unwrap it first + ╭─[template_string_decls.baml:37:15] + 37 │ `${name} ${age} ${items}` + · ───── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:37:16] + 37 │ `${name} ${age} ${items}` + · ─┬─ + · ╰── expected `T`, found `int | null` + ╰──── + + [type] E0001 + + × cannot interpolate a value of type `string[] | null` — it may be null; coalesce with `?? "…"` or unwrap it first + ╭─[template_string_decls.baml:37:22] + 37 │ `${name} ${age} ${items}` + · ─────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:37:23] + 37 │ `${name} ${age} ${items}` + · ──┬── + · ╰── expected `T`, found `string[] | null` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:52:5] + 52 │ `Hello ${x}` + · ──────────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:52:14] + 52 │ `Hello ${x}` + · ┬ + · ╰── expected `T`, found `string` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:56:5] + 56 │ ╭─▶ ` + 57 │ │ ${for (let item in items)} + 58 │ │ - ${item} + 59 │ │ ${endfor} + 60 │ ╰─▶ ` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:56:5] + 56 │ ╭─▶ ` + 57 │ │ ${for (let item in items)} + 58 │ │ - ${item} + 59 │ │ ${endfor} + 60 │ ╰─▶ ` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:58:13] + 58 │ - ${item} + · ──┬─ + · ╰── expected `T`, found `string` + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:58:13] + 58 │ - ${item} + · ──┬─ + · ╰── expected `T`, found `string` + ╰──── + + [type] E0002 + + × cannot infer type parameter `T` + ╭─[template_string_decls.baml:64:5] + 64 │ `${if (show)}${value}${else}hidden${endif}` + · ─────────────────────────────────────────── + ╰──── + + [type] E0001 + + × mismatched types + ╭─[template_string_decls.baml:64:20] + 64 │ `${if (show)}${value}${else}hidden${endif}` + · ──┬── + · ╰── expected `T`, found `string` + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__config_decls.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__config_decls.snap index ecef6e7a012..59dfa6bbd76 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__config_decls.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__config_decls.snap @@ -361,7 +361,7 @@ retry_policy TriviaRetryBlock { /* 6 open brace trailing */ // ===== Test declaration ===== function TestTarget() -> string { client: BasicClient - prompt: #"Hello"# + prompt: `Hello` } test BasicTest { diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__template_string_decls.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__template_string_decls.snap index 1dc71ecee46..9c33249a1ff 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__template_string_decls.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__10_formatter__template_string_decls.snap @@ -1,282 +1,78 @@ --- source: crates/baml_tests/src/generated_tests.rs --- -// Testing all the formatter cases for template string declarations -// as well as any special cases inside the template string - -// ===== Basic template string ===== -template_string BasicTemplate(name: string) #" - Hello, {{ name }}! -"# - -// ===== Template string with no params ===== -template_string NoParams() #" - A static template with no parameters. -"# - -// ===== Template string with multiple params ===== -template_string MultiParam(first: string, last: string, age: int) #" - Name: {{ first }} {{ last }} - Age: {{ age }} -"# - -// ===== Template string with complex param types ===== -template_string ComplexParams(items: string[], config: map) #" - Items: {{ items }} - Config: {{ config }} -"# - -// ===== Template string with single-line body ===== -template_string SingleLine(x: int) #"Value is {{ x }}"# - -// ===== Template string with many params that should wrap ===== -template_string ManyParams(first_parameter: string, second_parameter: string, third_parameter: int, fourth_parameter: bool) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} {{ fourth_parameter }} -"# - -// ===== Very long param list ===== -template_string VeryLongParams(extremely_long_parameter_name: string, another_very_long_parameter_name: int) #" - {{ extremely_long_parameter_name }} {{ another_very_long_parameter_name }} -"# - -// ===== Template with template comments ===== -template_string WithComment(x: string) #" - {# This is a template comment #} - Value: {{ x }} -"# - -// ===== Trivia on template string declaration (line comments) ===== -// 1 keyword leading -template_string TriviaTemplate( // 6 open paren trailing - // 7 param leading - x: string, // 12 type trailing - // 13 close paren leading -) #" - Hello {{ x }} -"# - -/////////////////// - -// ===== Trivia on template string declaration (block comments) ===== -/* 1 keyword leading */ -template_string TriviaTemplateBlock( /* 6 open paren trailing */ - /* 7 param leading */ - x: /* 9 colon trailing *//* 10 type leading */string, /* 11 type trailing */ - /* 12 close paren leading */ -) #" - Hello {{ x }} -"# - -/////////////////// - -// ===== Multi param trivia (line comments) ===== -template_string MultiParamTrivia( - // 1 first param leading - a: string, // 6 comma trailing - // 7 second param leading - b: int, // 11 second type trailing -) #" - {{ a }} {{ b }} -"# - -// ===== Multi param trivia (block comments) ===== -template_string MultiParamTriviaBlock( - /* 1 first param leading */ - a: /* 3 first colon trailing *//* 4 first type leading */string/* 5 first type trailing */, /* 6 comma trailing */ - /* 7 second param leading */ - b: /* 9 second colon trailing *//* 10 second type leading */int, /* 11 second type trailing */ -) #" - {{ a }} {{ b }} -"# - -// ===== Short name ===== -template_string T(x: int) #"{{ x }}"# - -// ===== Long name exceeding limit ===== -template_string ThisIsAnExtremelyLongTemplateStringNameThatDefinitelyExceedsTheDefaultLineLimitOfOneHundredColumns(x: int) #" - {{ x }} -"# - -// ===== Long name + many params exceeding limit ===== -template_string VeryLongTemplateNameWithManyParameters(first_parameter: string, second_parameter: int, third_parameter: bool, fourth_parameter: float) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} {{ fourth_parameter }} -"# - -// ===== Many params with complex types exceeding limit ===== -template_string ComplexTypesWrap( +// Formatter coverage migrated from template_string declarations to functions +// returning backtick strings. + +function BasicTemplate(name: string) -> string { + `Hello, ${name}!` +} + +function NoParams() -> string { + `A static template with no parameters.` +} + +function MultiParam(first: string, last: string, age: int) -> string { + ` + Name: ${first} ${last} + Age: ${age} + ` +} + +function ComplexParams(items: string[], config: map) -> string { + ` + Items: ${items} + Config: ${config} + ` +} + +function SingleLine(x: int) -> string { + `Value is ${x}` +} + +function ManyParams( + first_parameter: string, + second_parameter: string, + third_parameter: int, + fourth_parameter: bool, +) -> string { + `${first_parameter} ${second_parameter} ${third_parameter} ${fourth_parameter}` +} + +function ComplexTypesWrap( items: (int | string | bool)[], config: map, callback: (x: int, y: int) -> string, -) #" - {{ items }} {{ config }} -"# - -// ===== Very long param list with many complex params far exceeding limit ===== -template_string VeryLongComplexParams( - first_parameter: map, - second_parameter: (int | string)[], - third_parameter: map>, - fourth_parameter: (x: int) -> string, -) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -// ===== Params with deeply nested generic types exceeding limit ===== -template_string DeepGenericParams(data: map>>) #" - {{ data }} -"# - -// ===== Params with union types ===== -template_string UnionParams(result: string | int | bool | null, items: (string | int)[]) #" - {{ result }} {{ items }} -"# - -// ===== Params with optional types ===== -template_string OptionalParams(name: string?, age: int?, items: string[]?) #" - {{ name }} {{ age }} {{ items }} -"# - -// ===== Params with function types ===== -template_string FnTypeParams(transform: (x: int) -> int, combine: (a: string, b: string) -> string) #" - transformed -"# - -// ===== Long name + long complex params far exceeding limit ===== -template_string ExtremelyLongNamePlusComplexParameters( - first_very_long_parameter_name: map, - second_very_long_parameter_name: (int | string | bool | float | null)[], -) #" - {{ first_very_long_parameter_name }} {{ second_very_long_parameter_name }} -"# - -// ===== Six params all with long names and types exceeding limit ===== -template_string SixLongParams( - first_param: string, - second_param: int, - third_param: bool, - fourth_param: float, - fifth_param: string[], - sixth_param: map, -) #" - {{ first_param }} {{ second_param }} {{ third_param }} {{ fourth_param }} {{ fifth_param }} {{ sixth_param }} -"# - -// ===== Trivia on wrapping many params (line comments) ===== -// 1 keyword leading -template_string TriviaWrappingParams( // 6 open paren trailing - // 7 first param leading - first_parameter: map, // 12 comma trailing - // 13 second param leading - second_parameter: (int | string)[], // 18 comma trailing - // 19 third param leading - third_parameter: map>, // 23 third type trailing - // 24 close paren leading -) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping many params (block comments) ===== -/* 1 keyword leading */ -template_string TriviaWrappingParamsBlock( /* 6 open paren trailing */ - /* 7 first param leading */ - first_parameter: /* 9 first colon trailing *//* 10 first type leading */map< - string, - int | string | bool - >/* 11 first type trailing */, /* 12 comma trailing */ - /* 13 second param leading */ - second_parameter: /* 15 second colon trailing *//* 16 second type leading */(int | string)[]/* 17 second type trailing */, /* 18 comma trailing */ - /* 19 third param leading */ - third_parameter: /* 21 third colon trailing *//* 22 third type leading */map< - string, - map - >, /* 23 third type trailing */ - /* 24 close paren leading */ -) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping long name + params (line comments) ===== -// 1 keyword leading -template_string TriviaVeryLongTemplateNameWithParameters( // 6 open paren trailing - // 7 first param leading - first_parameter: string, // 12 comma trailing - // 13 second param leading - second_parameter: int, // 18 comma trailing - // 19 third param leading - third_parameter: bool, // 23 third type trailing - // 24 close paren leading -) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on wrapping long name + params (block comments) ===== -/* 1 keyword leading */ -template_string TriviaVeryLongTemplateNameWithParametersBlock( /* 6 open paren trailing */ - /* 7 first param leading */ - first_parameter: /* 9 first colon trailing *//* 10 first type leading */string/* 11 first type trailing */, /* 12 comma trailing */ - /* 13 second param leading */ - second_parameter: /* 15 second colon trailing *//* 16 second type leading */int/* 17 second type trailing */, /* 18 comma trailing */ - /* 19 third param leading */ - third_parameter: /* 21 third colon trailing *//* 22 third type leading */bool, /* 23 third type trailing */ - /* 24 close paren leading */ -) #" - {{ first_parameter }} {{ second_parameter }} {{ third_parameter }} -"# - -/////////////////// - -// ===== Trivia on param with deeply nested generic type (line comments) ===== -template_string TriviaDeepGenericParam( - // 1 param leading - data: map< // 6 open angle trailing - // 7 key type leading - string, // 9 comma trailing - // 10 value type leading - map< // 12 inner open angle trailing - // 13 inner key leading - string, // 15 inner comma trailing - // 16 inner value leading - int // 17 inner value trailing - | string // 20 union member trailing - > // 21 inner close angle trailing - >, // 22 outer close angle trailing -) #" - {{ data }} -"# - -// ===== Trivia on param with deeply nested generic type (block comments) ===== -template_string TriviaDeepGenericParamBlock( - /* 1 param leading */ - data: /* 3 colon trailing *//* 4 type leading */map< /* 6 open angle trailing */ - /* 7 key type leading */ - string/* 8 key type trailing */, /* 9 comma trailing */ - /* 10 value type leading */ - map< /* 12 inner open angle trailing */ - /* 13 inner key leading */ - string/* 14 inner key trailing */, /* 15 inner comma trailing */ - /* 16 inner value leading */ - int /* 17 inner value trailing */ - |/* 18 pipe trailing *//* 19 union member leading */string /* 20 union member trailing */ - > /* 21 inner close angle trailing */ - >, /* 22 outer close angle trailing */ -) #" - {{ data }} -"# - -// ===== Extraneous whitespace ===== -template_string ExtraWhitespace(a: string, b: int) #" - {{ a }} {{ b }} -"# - -// ===== Template with control flow ===== -template_string WithControl(items: string[]) #" - {% for item in items %} - - {{ item }} - {% endfor %} -"# +) -> string { + `${items} ${config}` +} + +function OptionalParams(name: string?, age: int?, items: string[]?) -> string { + `${name} ${age} ${items}` +} + +function FnTypeParams( + transform: (x: int) -> int, + combine: (a: string, b: string) -> string, +) -> string { + `transformed` +} + +// Trivia around a function returning a backtick string. +function TriviaTemplate( // open paren trailing + x: string, +) -> string { + `Hello ${x}` +} + +function WithControl(items: string[]) -> string { + ` + ${for (let item in items)} + - ${item} + ${endfor} + ` +} + +function WithConditional(show: bool, value: string) -> string { + `${if (show)}${value}${else}hidden${endif}` +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__03_ppir.snap new file mode 100644 index 00000000000..0663c0cdcaa --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__03_ppir.snap @@ -0,0 +1,5 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== PPIR === +function user.Greet(name: string, client: ai.Client? = null) -> string [llm] diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__04_tir.snap new file mode 100644 index 00000000000..da37c0436f4 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__04_tir.snap @@ -0,0 +1,4 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== TIR2 === diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap new file mode 100644 index 00000000000..a331635ca13 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== COMPILER2 DIAGNOSTICS === + [hir] E0010 + + × LLM function `Greet` uses a Jinja `#"..."#` prompt, which is no longer supported. Use a backtick prompt with `${...}` interpolation instead. + ╭─[main.baml:3:12] + 3 │ prompt #"Hello {{ name }}"# + · ──────────┬───────── + · ╰── replace `#"Hello {{ name }}"#` with `` `Hello ${name}` `` + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__10_formatter__main.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__10_formatter__main.snap new file mode 100644 index 00000000000..dc1bbce5e18 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__10_formatter__main.snap @@ -0,0 +1,7 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +function Greet(name: string) -> string { + client: "openai/gpt-4o" + prompt: #"Hello {{ name }}"# +} diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__03_ppir.snap index 7101256d9eb..899b9ca0c7e 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__03_ppir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__03_ppir.snap @@ -1,6 +1,5 @@ --- source: crates/baml_tests/src/generated_tests.rs -assertion_line: 52309 --- === PPIR === function user.BadDefaultPositional() -> string [expr] { @@ -52,6 +51,9 @@ function user.Current(self: ? = null) -> int [expr] { function user.Filtered(query: string, filter: string? = null) -> int [expr] { { } query.length() } +function user.Greeting(name: string = "world") -> string [expr] { + { } `...` +} function user.LambdaDefault() -> int [expr] { { let f = (x: int = 1) -> int { { } x } } f(2) } diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__04_tir.snap index c5f20b6e5ce..67f71a1217c 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__04_tir.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__04_tir.snap @@ -64,6 +64,11 @@ function user.BadRequiredAfterDefault(a: int = 1, b: int) -> int throws never { } !! 794..800: required parameter `b` cannot appear after a defaulted parameter } +function user.Greeting(name: string = "world") -> string throws never { + { : string + `...` : string + } +} type user.NamedFilter = (query: string, filter?: string | null) -> int throws never type user.NamedLimit = (query: string, limit?: string | null) -> int throws never class user.Counter { @@ -73,7 +78,7 @@ function user.Counter.Current(self: user.Counter = null) -> int throws never { { : int self.value : int } - !! 271..275: `self` cannot have a default value + !! 277..281: `self` cannot have a default value } function user.LambdaDefault() -> int throws never { { : int @@ -97,7 +102,7 @@ function user.OptionalNameMismatch() -> int throws never { let f: NamedLimit = Filtered : (query: string, filter?: string | null) -> int throws never -> (query: string, limit?: string | null) -> int throws never f("cats", limit = "recent") : int } - !! 537..545: type mismatch: expected NamedLimit, got (query: string, filter?: string | null) -> int throws never + !! 543..551: type mismatch: expected NamedLimit, got (query: string, filter?: string | null) -> int throws never } type user.NamedFilter$stream = unknown type user.NamedLimit$stream = unknown diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__05_diagnostics.snap index 7e14c9051f5..526c793cf39 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__05_diagnostics.snap @@ -58,28 +58,19 @@ source: crates/baml_tests/src/generated_tests.rs · ────── ╰──── - [hir] E0010 - - × parameter defaults are not supported in template_string `Greeting` - ╭─[unsupported_contexts.baml:1:41] - 1 │ template_string Greeting(name: string = "world") #"hello {{ name }}"# - · ───┬─── - · ╰── default value is not allowed here - ╰──── - [type] E0001 × `self` cannot have a default value - ╭─[unsupported_contexts.baml:9:27] - 9 │ function Current(self = null) -> int { - · ──── - ╰──── + ╭─[unsupported_contexts.baml:11:27] + 11 │ function Current(self = null) -> int { + · ──── + ╰──── [hir] E0010 × parameter defaults are not supported in lambda parameters - ╭─[unsupported_contexts.baml:15:21] - 15 │ let f = (x: int = 1) -> int { x } + ╭─[unsupported_contexts.baml:17:21] + 17 │ let f = (x: int = 1) -> int { x } · ┬ · ╰── default value is not allowed here ╰──── @@ -87,8 +78,8 @@ source: crates/baml_tests/src/generated_tests.rs [type] E0001 × mismatched types - ╭─[unsupported_contexts.baml:24:23] - 24 │ let f: NamedLimit = Filtered + ╭─[unsupported_contexts.baml:26:23] + 26 │ let f: NamedLimit = Filtered · ────┬─── · ╰── expected `NamedLimit`, found `(query: string, filter?: string | null) -> int throws never` ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__10_formatter__unsupported_contexts.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__10_formatter__unsupported_contexts.snap index c1d9094f7d3..8fadfc1847b 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__10_formatter__unsupported_contexts.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/optional_parameter_defaults/baml_tests__diagnostic_errors__optional_parameter_defaults__10_formatter__unsupported_contexts.snap @@ -1,7 +1,9 @@ --- source: crates/baml_tests/src/generated_tests.rs --- -template_string Greeting(name: string = "world") #"hello {{ name }}"# +function Greeting(name: string = "world") -> string { + `hello ${name}` +} type NamedFilter = (query: string, filter?: string?) -> int throws never; diff --git a/baml_language/crates/baml_tests/src/compiler2_hir.rs b/baml_language/crates/baml_tests/src/compiler2_hir.rs index b618a5c58ed..50d6acbbe35 100644 --- a/baml_language/crates/baml_tests/src/compiler2_hir.rs +++ b/baml_language/crates/baml_tests/src/compiler2_hir.rs @@ -2096,7 +2096,7 @@ implements MyIface for MyClass { function m(self) -> int { 1 } } -template_string MyTemplate(x: string) #"{{ x }}"# +function MyTemplate(x: string) -> string { `${x}` } client MyClient = openai.OpenAiClient.new(model = "gpt-4o-mini"); @@ -2154,12 +2154,12 @@ test my_test { "MyIface" ); - let template = *item_data::file_template_strings(&db, file) + let template = *item_data::file_functions(&db, file) .iter() - .find(|&&t| item_data::template_string_data(&db, t).name.as_str() == "MyTemplate") + .find(|&&f| item_data::function_data(&db, f).name.as_str() == "MyTemplate") .unwrap(); assert_eq!( - text(item_data::template_string_source_map(&db, template).name_span), + text(item_data::function_source_map(&db, template).name_span), "MyTemplate" ); diff --git a/baml_language/crates/baml_tests/src/incremental/scenarios.rs b/baml_language/crates/baml_tests/src/incremental/scenarios.rs index 15b877adeb9..4539f736dc5 100644 --- a/baml_language/crates/baml_tests/src/incremental/scenarios.rs +++ b/baml_language/crates/baml_tests/src/incremental/scenarios.rs @@ -30,7 +30,7 @@ fn editing_function_body_preserves_item_tree() { r##" function Greet(name: string) -> string { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` } "##, ); @@ -47,7 +47,7 @@ function Greet(name: string) -> string { file.set_text(test_db.db_mut()).to(r##" function Greet(name: string) -> string { client GPT4 - prompt #"Hi there {{name}}!"# + prompt `Hi there ${name}!` } "## .to_string()); @@ -73,7 +73,7 @@ fn renaming_function_invalidates_item_tree() { r##" function OldName(x: string) -> string { client GPT4 - prompt #"test"# + prompt `test` } "##, ); @@ -90,7 +90,7 @@ function OldName(x: string) -> string { file.set_text(test_db.db_mut()).to(r##" function NewName(x: string) -> string { client GPT4 - prompt #"test"# + prompt `test` } "## .to_string()); @@ -266,7 +266,7 @@ fn type_inference_cached_on_no_change() { r##" function Greet(name: string) -> string { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` } "##, ); @@ -287,7 +287,7 @@ fn type_inference_cached_on_whitespace_change() { "test.baml", r##"function Greet(name: string) -> string { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` }"##, ); @@ -298,7 +298,7 @@ fn type_inference_cached_on_whitespace_change() { file.set_text(test_db.db_mut()) .to(r##"function Greet(name: string) -> string { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` } @@ -324,7 +324,7 @@ fn type_inference_invalidated_on_signature_change() { r##" function Greet(name: string) -> string { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` } "##, ); @@ -336,7 +336,7 @@ function Greet(name: string) -> string { file.set_text(test_db.db_mut()).to(r##" function Greet(name: string) -> int { client GPT4 - prompt #"Hello {{name}}"# + prompt `Hello ${name}` } "## .to_string()); @@ -928,7 +928,7 @@ fn editing_a_function_prompt_preserves_its_llm_meta() { let file = test_db.db_mut().add_file( "test.baml", - "function Greet(name: string) -> string {\n client GPT4\n prompt #\"Hi {{name}}\"#\n}\n", + "function Greet(name: string) -> string {\n client GPT4\n prompt `Hi ${name}`\n}\n", ); let before = { @@ -940,7 +940,7 @@ fn editing_a_function_prompt_preserves_its_llm_meta() { // Rewrite only the prompt — the client (the one fact the projection keeps) is // untouched. file.set_text(test_db.db_mut()).to( - "function Greet(name: string) -> string {\n client GPT4\n prompt #\"Hello there {{name}}!\"#\n}\n" + "function Greet(name: string) -> string {\n client GPT4\n prompt `Hello there ${name}!`\n}\n" .to_string(), ); diff --git a/baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs b/baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs index 1d7cb22006e..373312a8fd9 100644 --- a/baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs +++ b/baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs @@ -61,10 +61,10 @@ fn formatting_preserves_backtick_values() { Reindented, "function Demo(name: string) -> string {\n client \"openai/gpt-4o\"\n prompt `\n Hello ${name}\n Goodbye\n `\n}\n", ), - // template_string body + // function return value ( Reindented, - "template_string Header(title: string) `\n # ${title}\n`\n", + "function Header(title: string) -> string {\n `\n # ${title}\n `\n}\n", ), // attribute argument ( diff --git a/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs b/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs index 7e58fb6bb87..c99aded7a1d 100644 --- a/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs +++ b/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs @@ -69,7 +69,7 @@ fn throw_of_non_literal_expression_compiles() { fn generic_llm_function_with_generic_return_compiles() { let db = db_with( "class Box { value T }\n\ - function Extract(text: string) -> Box { client GPT4\nprompt #\"x\"# }\n", + function Extract(text: string) -> Box { client GPT4\nprompt `x` }\n", ); assert!(bytecode_ok(&db).is_ok()); } diff --git a/baml_language/crates/baml_tests/tests/incremental_typing_repro.rs b/baml_language/crates/baml_tests/tests/incremental_typing_repro.rs index 70c7d419aff..4e1cf1d0147 100644 --- a/baml_language/crates/baml_tests/tests/incremental_typing_repro.rs +++ b/baml_language/crates/baml_tests/tests/incremental_typing_repro.rs @@ -80,26 +80,26 @@ class GuessResponse { function GenerateFamousPersonName(previous_names: string[]) -> string { client GPT4o - prompt #" - {{ previous_names }} - "# + prompt ` + ${previous_names} + ` } function SimulateHumanGuess(history: string[]) -> string { client GPT4o - prompt #" - {{ history }} - "# + prompt ` + ${history} + ` } function TakeGuess(user_guess: string, famous_person_name: string, history: string[]) -> GuessResponse { client GPT4o - prompt #" - {{ user_guess }} - {{ famous_person_name }} - {{ history }} - {{ ctx.output_format }} - "# + prompt ` + ${user_guess} + ${famous_person_name} + ${history} + ${ctx.output_format} + ` } function GuessGameAgent() -> GuessResponse { diff --git a/baml_language/crates/baml_tests/tests/interfaces.rs b/baml_language/crates/baml_tests/tests/interfaces.rs index 2a262e55985..67ee0586f71 100644 --- a/baml_language/crates/baml_tests/tests/interfaces.rs +++ b/baml_language/crates/baml_tests/tests/interfaces.rs @@ -3736,10 +3736,10 @@ fn llm_function_can_return_interface_type() { } function detect_animal(description: string) -> Animal { client GPT4o - prompt #" - Identify the animal from the description: {{description}}. - {{ ctx.output_format }} - "# + prompt ` + Identify the animal from the description: ${description}. + ${ctx.output_format} + ` } "##, ); @@ -3750,9 +3750,7 @@ fn llm_function_returning_interface_enumerates_implementors_in_schema() { // BEP-044 §"LLM Functions": a function declared to return an // interface must compile, with the schema-rendering side later // expanding the interface into a `oneOf` of its implementors at - // prompt evaluation time. This test pins the type-check surface; - // the prompt-rendering snapshot would require a separate harness - // that captures the rendered Jinja output. + // prompt evaluation time. This test pins the type-check surface. assert_no_interface_errors( r##" client GPT4o { @@ -3774,10 +3772,10 @@ fn llm_function_returning_interface_enumerates_implementors_in_schema() { } function detect_animal(description: string) -> Animal { client GPT4o - prompt #" - Identify the animal: {{ description }}. - {{ ctx.output_format }} - "# + prompt ` + Identify the animal: ${description}. + ${ctx.output_format} + ` } "##, ); @@ -4897,10 +4895,10 @@ fn llm_function_with_interface_array_return_compiles() { } function detect_zoo(description: string) -> Animal[] { client GPT4o - prompt #" - Identify every animal mentioned in {{description}}. - {{ ctx.output_format }} - "# + prompt ` + Identify every animal mentioned in ${description}. + ${ctx.output_format} + ` } "##, ); @@ -4922,11 +4920,11 @@ fn llm_function_with_interface_in_union_return_compiles() { } function detect_or_describe(description: string) -> Animal | string { client GPT4o - prompt #" - If {{description}} clearly identifies an animal, return one. + prompt ` + If ${description} clearly identifies an animal, return one. Otherwise, paraphrase the description. - {{ ctx.output_format }} - "# + ${ctx.output_format} + ` } "##, ); @@ -4952,10 +4950,10 @@ fn llm_function_takes_interface_typed_parameter_compiles() { } function describe_animal(a: Animal) -> string { client GPT4o - prompt #" - Describe the animal named {{a.name}}. - {{ ctx.output_format }} - "# + prompt ` + Describe the animal named ${a.name}. + ${ctx.output_format} + ` } "##, ); diff --git a/baml_language/crates/baml_tests/tests/prompt_tag_e2e.rs b/baml_language/crates/baml_tests/tests/prompt_tag_e2e.rs index 84d0aa51eb1..042f56e8c8b 100644 --- a/baml_language/crates/baml_tests/tests/prompt_tag_e2e.rs +++ b/baml_language/crates/baml_tests/tests/prompt_tag_e2e.rs @@ -5,9 +5,6 @@ //! request built by the legacy `call_llm_function` orchestrator; prompt //! rendering is now covered by the `$render_prompt` companion tests in //! `baml_src/ns_prompt_tag_runtime/`. -//! - `backtick_and_jinja_prompts_produce_identical_messages` — Jinja -//! `#"..."#` prompts are a compile error now, so there is no legacy twin -//! to compare against. //! //! The remaining tests exercise the ai-world `$stream` companion against a //! local OpenAI Responses API endpoint. diff --git a/baml_language/crates/baml_type/src/defs.rs b/baml_language/crates/baml_type/src/defs.rs index e0e5bccf9d9..2154f9cd662 100644 --- a/baml_language/crates/baml_type/src/defs.rs +++ b/baml_language/crates/baml_type/src/defs.rs @@ -75,11 +75,8 @@ pub struct ParamDef { /// The kind of function body — determines how the runtime dispatches it. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FunctionBodyKind { - /// Declarative LLM function with prompt template and client reference. - Llm { - prompt_template: String, - client: String, - }, + /// Declarative LLM function with a client reference. + Llm { client: String }, /// Imperative expression function — compiled to bytecode. Expr, /// Function body is missing (error recovery in HIR). diff --git a/baml_language/crates/bex_engine/src/lib.rs b/baml_language/crates/bex_engine/src/lib.rs index 134ab0d0818..dc192ea4dea 100644 --- a/baml_language/crates/bex_engine/src/lib.rs +++ b/baml_language/crates/bex_engine/src/lib.rs @@ -307,8 +307,8 @@ pub struct UserFunctionInfo { /// Exposed for BEP-027 §"`baml.argv`": `argv[1]` under root-main `baml run` /// is the path to the file containing `main`. pub source_file: String, - /// `true` when the function carries `FunctionMeta::Llm` — i.e. it - /// was declared with `client X { ... } prompt #"..."#` and the + /// `true` when the function carries `FunctionMeta::Llm` and was declared + /// with an LLM client and backtick prompt. /// compiler synthesized the LLM dispatch body. Surfaced here so /// `baml run --list` can annotate LLM functions inline without /// reaching back into the heap to inspect `body_meta`. @@ -1797,7 +1797,6 @@ impl BexEngine { let sys_op_ctx = sys_types::EngineSysOpContext { llm_functions: Arc::new(llm_functions), function_global_indices: Arc::new(bytecode.function_global_indices), - template_strings_macros: Arc::new(bytecode.template_strings_macros), class_definitions: Arc::new(class_definitions), enum_definitions: Arc::new(enum_definitions), type_alias_definitions: Arc::new(bex_vm::package_load::all_recursive_type_aliases( @@ -2026,15 +2025,10 @@ impl BexEngine { // SAFETY: ptr is from resolved_function_names, a compile-time object let obj = unsafe { ptr.get() }; if let Object::Function(func) = obj { - if let Some(FunctionMeta::Llm { - prompt_template, - client, - }) = &func.body_meta - { + if let Some(FunctionMeta::Llm { client }) = &func.body_meta { llm_functions.insert( name.clone(), sys_types::LlmFunctionInfo { - prompt_template: prompt_template.clone(), client_name: client.clone(), return_type: declared_symbolic(&func.return_type, func), }, diff --git a/baml_language/crates/bex_engine/tests/llm_render.rs b/baml_language/crates/bex_engine/tests/llm_render.rs index bc40d82a640..e9da5eb7cd7 100644 --- a/baml_language/crates/bex_engine/tests/llm_render.rs +++ b/baml_language/crates/bex_engine/tests/llm_render.rs @@ -1,215 +1,88 @@ //! Integration tests for LLM prompt rendering. //! -//! Rust-level tests drive `sys_llm` template rendering directly; BAML-level -//! tests drive the compiler-generated `$render_prompt` companion, which +//! Tests drive the compiler-generated `$render_prompt` companion, which //! renders the spec's prompt (with the return type's output format) as a //! structural `ai.Prompt`. -//! -//! Removed with the legacy LLM path (see git history): the -//! `baml.prompt.render_prompt`/`build_request`/`call_llm_function` builtin flow -//! over declared `client` blocks and Jinja prompts, and the -//! `template_string`-in-prompt expansion tests (`template_string` calls do not -//! bind inside ai-world backtick prompts). - -use baml_builtins2::{PromptAst as BuiltinPromptAst, PromptAstSimple}; -use baml_type::TyAttr; -use bex_engine::{FunctionCallContextBuilder, RuntimeTy}; + +use bex_engine::FunctionCallContextBuilder; use bex_heap::BexExternalValue; -#[tokio::test] -async fn test_render_prompt_directly() { - use indexmap::IndexMap; - - // Test the Jinja rendering directly - let template = "Hello, {{ name }}! You are {{ age }} years old."; - let mut args = IndexMap::new(); - args.insert( - "name".to_string(), - BexExternalValue::String("Alice".to_string().into()), - ); - args.insert("age".to_string(), BexExternalValue::Int(30)); - - let client = - sys_llm::baml_std::PrimitiveClient::new("test".to_string(), "openai".to_string(), { - sys_llm::baml_std::PrimitiveClientOptions { - model: Some("gpt-4o".to_string()), - default_role: Some("user".to_string()), - allowed_roles: Some(vec![ - "user".to_string(), - "assistant".to_string(), - "system".to_string(), - ]), - ..Default::default() - } - }) - .unwrap(); - - let ctx = sys_llm::RenderContext { - client: sys_llm::RenderContextClient { - name: client.name.clone(), - provider: client.provider.clone(), - default_role: client.default_role.clone(), - allowed_roles: client.allowed_roles, - }, - output_format: sys_llm::OutputFormatContent::new(RuntimeTy::String { - attr: TyAttr::default(), - }), - tags: IndexMap::new(), - enums: std::collections::HashMap::new(), - }; +mod common; - let result = sys_llm::render_prompt(template, &args, &ctx).unwrap(); +use common::{EngineProgram, assert_engine_executes}; - match result { - BuiltinPromptAst::Simple(s) => { - assert_eq!( - s, - std::sync::Arc::new("Hello, Alice! You are 30 years old.".to_string().into()) - ); - } - _ => panic!("Expected string result"), - } +#[tokio::test] +async fn backtick_prompt_interpolates_arguments() -> anyhow::Result<()> { + assert_engine_executes(EngineProgram { + source: r#" +function Greet(name: string, age: int) -> string { + client "openai/gpt-4o" + prompt `Hello, ${name}! You are ${age} years old.` } -#[tokio::test] -async fn test_render_prompt_with_chat_roles() { - use indexmap::IndexMap; - - let template = r#" -{{ _.role("system") }} -You are a helpful assistant. -{{ _.role("user") }} -{{ question }} -"#; - let mut args = IndexMap::new(); - args.insert( - "question".to_string(), - BexExternalValue::String("What is 2+2?".to_string().into()), - ); +function main() -> string { + Greet$render_prompt("Alice", 30).text() +} +"#, + entry: "main", + expected: Ok(BexExternalValue::from( + "Hello, Alice! You are 30 years old.", + )), + ..Default::default() + }) + .await +} - let client = - sys_llm::baml_std::PrimitiveClient::new("test".to_string(), "openai".to_string(), { - sys_llm::baml_std::PrimitiveClientOptions { - model: Some("gpt-4o".to_string()), - default_role: Some("user".to_string()), - allowed_roles: Some(vec![ - "user".to_string(), - "assistant".to_string(), - "system".to_string(), - ]), - ..Default::default() - } - }) - .unwrap(); - - let ctx = sys_llm::RenderContext { - client: sys_llm::RenderContextClient { - name: client.name.clone(), - provider: client.provider.clone(), - default_role: client.default_role.clone(), - allowed_roles: client.allowed_roles, - }, - output_format: sys_llm::OutputFormatContent::new(RuntimeTy::String { - attr: TyAttr::default(), - }), - tags: IndexMap::new(), - enums: std::collections::HashMap::new(), - }; +#[tokio::test] +async fn backtick_prompt_builds_chat_roles() -> anyhow::Result<()> { + assert_engine_executes(EngineProgram { + source: r#" +function Answer(question: string) -> string { + client "openai/gpt-4o" + prompt `${role("system")}You are a helpful assistant.${role("user")}${question}` +} - let result = sys_llm::render_prompt(template, &args, &ctx).unwrap(); - - // Result should be a Vec of messages - match result { - BuiltinPromptAst::Vec(messages) => { - assert_eq!(messages.len(), 2); - - // Check first message (system) - match messages[0].as_ref() { - BuiltinPromptAst::Message { role, content, .. } => { - assert_eq!(role, "system"); - match content.as_ref() { - PromptAstSimple::String(s) => { - assert!(s.contains("helpful assistant")); - } - _ => panic!("Expected string content"), - } - } - _ => panic!("Expected message"), - } - - // Check second message (user) - match messages[1].as_ref() { - BuiltinPromptAst::Message { role, content, .. } => { - assert_eq!(role, "user"); - match content.as_ref() { - PromptAstSimple::String(s) => { - assert!(s.contains("What is 2+2?")); - } - _ => panic!("Expected string content"), - } - } - _ => panic!("Expected message"), - } - } - _ => panic!("Expected Vec of messages, got {result:?}"), +function main() -> string { + let out = "" + for (let message in Answer$render_prompt("What is 2+2?").messages()) { + out += message.role + "=" + message.content + ";" } + out +} +"#, + entry: "main", + expected: Ok(BexExternalValue::from( + "system=You are a helpful assistant.;user=What is 2+2?;", + )), + ..Default::default() + }) + .await } #[tokio::test] -async fn test_render_prompt_with_enums() { - use indexmap::IndexMap; - use sys_llm::{RenderEnum, RenderEnumVariant}; - - let template = "Category: {{ ctx.enums.Category.SPORTS }}"; - let args = IndexMap::new(); - - let mut enums = std::collections::HashMap::new(); - enums.insert( - "Category".to_string(), - RenderEnum { - name: "Category".to_string(), - variants: vec![ - RenderEnumVariant { - name: "SPORTS".to_string(), - }, - RenderEnumVariant { - name: "TECH".to_string(), - }, - RenderEnumVariant { - name: "POLITICS".to_string(), - }, - ], - }, - ); - - let ctx = sys_llm::RenderContext { - client: sys_llm::RenderContextClient { - name: "test".to_string(), - provider: "openai".to_string(), - default_role: "user".to_string(), - allowed_roles: vec!["user".to_string()], - }, - output_format: sys_llm::OutputFormatContent::new(RuntimeTy::String { - attr: TyAttr::default(), - }), - tags: IndexMap::new(), - enums, - }; - - let result = sys_llm::render_prompt(template, &args, &ctx).unwrap(); +async fn backtick_prompt_interpolates_enum_values() -> anyhow::Result<()> { + assert_engine_executes(EngineProgram { + source: r#" +enum Category { + SPORTS + TECH +} - match result { - BuiltinPromptAst::Simple(s) => { - let PromptAstSimple::String(s) = s.as_ref() else { - panic!("Expected string content"); - }; - assert_eq!(s, "Category: SPORTS"); - } - _ => panic!("Expected string result"), - } +function Categorize(category: Category) -> string { + client "openai/gpt-4o" + prompt `Category: ${category}` } -mod common; +function main() -> string { + Categorize$render_prompt(Category.SPORTS).text() +} +"#, + entry: "main", + expected: Ok(BexExternalValue::from("Category: SPORTS")), + ..Default::default() + }) + .await +} #[tokio::test] async fn test_render_prompt_e2e_includes_output_format_schema() { diff --git a/baml_language/crates/bex_vm/src/vm.rs b/baml_language/crates/bex_vm/src/vm.rs index 010ab2cdd1d..d0f8a8eb776 100644 --- a/baml_language/crates/bex_vm/src/vm.rs +++ b/baml_language/crates/bex_vm/src/vm.rs @@ -888,8 +888,6 @@ pub struct BytecodeProgram { /// Maps function names to their global indices. /// Used for dynamic function lookup at runtime. pub function_global_indices: HashMap, - /// Pre-formatted Jinja `{% macro %}` definitions for all `template_strings`. - pub template_strings_macros: String, /// Client build metadata, passed through to `SysOpContext`. pub client_metadata: HashMap, /// Compiled test cases. @@ -940,7 +938,6 @@ pub fn convert_program(program: bex_vm_types::Program) -> Result Result { sort_packages(&mut program); // ---- Whole-program tails (design §3b step 5) ---------------------------- - // Template macros are joined by newlines, in unit (file) order. - let mut macros: Vec<&str> = Vec::new(); - for unit in units { - for m in &unit.template_macros { - macros.push(m.as_str()); - } - } - program.template_strings_macros = macros.join("\n"); - for unit in units { program.test_cases.extend(unit.test_cases.iter().cloned()); } @@ -901,7 +892,6 @@ mod tests { globals: vec![("user.foo".to_string(), 0), ("user.bar".to_string(), 1)], }, package_fragment: ProgramPackageFrag::default(), - template_macros: Vec::new(), test_cases: Vec::new(), callable_throws_fragment: Vec::new(), init_tail: None, @@ -1018,7 +1008,6 @@ mod tests { globals: vec![("a.f".to_string(), 0)], }, package_fragment: ProgramPackageFrag::default(), - template_macros: Vec::new(), test_cases: Vec::new(), callable_throws_fragment: Vec::new(), init_tail: None, @@ -1041,7 +1030,6 @@ mod tests { globals: vec![("b.g".to_string(), 0)], }, package_fragment: ProgramPackageFrag::default(), - template_macros: Vec::new(), test_cases: Vec::new(), callable_throws_fragment: Vec::new(), init_tail: None, diff --git a/baml_language/crates/bex_vm_types/src/types.rs b/baml_language/crates/bex_vm_types/src/types.rs index c8abb21f303..d5e1580f95b 100644 --- a/baml_language/crates/bex_vm_types/src/types.rs +++ b/baml_language/crates/bex_vm_types/src/types.rs @@ -82,10 +82,6 @@ pub struct Program { /// until `$init` runs at load time via `StoreGlobal`. pub let_global_indices: HashMap, - /// Pre-formatted Jinja `{% macro %}` definitions for all `template_strings`. - /// Prepended to function prompt templates by `get_jinja_template`. - pub template_strings_macros: String, - /// Client build metadata for constructing full client trees at runtime. /// Keyed by client name. pub client_metadata: HashMap, diff --git a/baml_language/crates/bex_vm_types/src/types/function.rs b/baml_language/crates/bex_vm_types/src/types/function.rs index d59a8d75e62..fa0455db53c 100644 --- a/baml_language/crates/bex_vm_types/src/types/function.rs +++ b/baml_language/crates/bex_vm_types/src/types/function.rs @@ -93,10 +93,7 @@ impl BorshDeserialize for FunctionKind { /// LLM-specific metadata for a function. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub enum FunctionMeta { - Llm { - prompt_template: String, - client: String, - }, + Llm { client: String }, } #[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] diff --git a/baml_language/crates/bex_vm_types/src/unit.rs b/baml_language/crates/bex_vm_types/src/unit.rs index 432f505caf1..4b659944be8 100644 --- a/baml_language/crates/bex_vm_types/src/unit.rs +++ b/baml_language/crates/bex_vm_types/src/unit.rs @@ -206,8 +206,6 @@ pub struct CompilationUnit { // --- side-table fragments the whole-program passes consume at link --- /// This unit's symbolic contribution to its package's structure. pub package_fragment: ProgramPackageFrag, - /// Pass-5 template-string `{% macro %}` fragments defined in this file. - pub template_macros: Vec, /// Pass-8 compiled test cases defined in this file. pub test_cases: Vec, /// `borsh(CallableThrowsFragment)` for this file. Opaque bytes diff --git a/baml_language/crates/sys_jinja/Cargo.toml b/baml_language/crates/sys_jinja/Cargo.toml deleted file mode 100644 index c5f13880150..00000000000 --- a/baml_language/crates/sys_jinja/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "sys_jinja" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true - -[dependencies] -minijinja = { workspace = true } -minijinja-contrib = { workspace = true } -regex = { workspace = true } -thiserror = { workspace = true } - -[lints] -workspace = true diff --git a/baml_language/crates/sys_jinja/src/lib.rs b/baml_language/crates/sys_jinja/src/lib.rs deleted file mode 100644 index 4e3785a0bb7..00000000000 --- a/baml_language/crates/sys_jinja/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::collections::HashSet; - -use minijinja::{Environment, value::Value}; - -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct JinjaError(#[from] minijinja::Error); - -/// Create the `MiniJinja` environment used by BAML prompt rendering. -/// -/// Runtime rendering and editor diagnostics both go through this function so -/// filters, syntax options, pycompat behavior, and formatter behavior do not -/// drift apart. -pub fn create_environment() -> Environment<'static> { - let mut env = Environment::new(); - - env.set_debug(true); - env.set_trim_blocks(true); - env.set_lstrip_blocks(true); - - env.add_filter("regex_match", regex_match); - env.add_filter("sum", sum); - - env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback); - - env.set_formatter(|out, _state, value| { - if value.is_none() || value.is_undefined() { - write!(out, "null").map_err(|e| { - minijinja::Error::new(minijinja::ErrorKind::WriteFailure, e.to_string()) - }) - } else { - write!(out, "{value}").map_err(|e| { - minijinja::Error::new(minijinja::ErrorKind::WriteFailure, e.to_string()) - }) - } - }); - - env -} - -/// Return root names `MiniJinja` would need from the render context for a prompt. -/// -/// `MiniJinja` does not subtract environment globals during this analysis, so -/// callers still decide which BAML names are in scope. -pub fn undeclared_prompt_variables(template: &str) -> Result, JinjaError> { - let mut env = create_environment(); - env.add_template("prompt", template)?; - Ok(env.get_template("prompt")?.undeclared_variables(false)) -} - -/// Filter: `regex_match` - Returns true if value matches regex pattern. -fn regex_match(value: &str, pattern: &str) -> bool { - regex::Regex::new(pattern) - .map(|re| re.is_match(value)) - .unwrap_or(false) -} - -/// Filter: sum - Sum numeric values in a list. -#[allow(clippy::cast_precision_loss)] -fn sum(values: Vec) -> Value { - let mut int_sum: i64 = 0; - let mut float_sum: f64 = 0.0; - let mut has_float = false; - - for val in values { - if let Ok(i) = i64::try_from(val.clone()) { - int_sum += i; - } else if let Ok(f) = f64::try_from(val) { - float_sum += f; - has_float = true; - } - } - - if has_float { - Value::from(int_sum as f64 + float_sum) - } else { - Value::from(int_sum) - } -} - -#[cfg(test)] -mod tests { - use super::undeclared_prompt_variables; - - #[test] - fn undeclared_prompt_variables_uses_runtime_jinja_semantics() { - let vars = undeclared_prompt_variables( - r#" - {{ _.role("user") }} - {{ name | lower }} - {% for item in items %} - {{ item.title }} - {% endfor %} - {{ ctx.output_format }} - "#, - ) - .unwrap(); - - assert!(vars.contains("_")); - assert!(vars.contains("ctx")); - assert!(vars.contains("name")); - assert!(vars.contains("items")); - assert!(!vars.contains("item")); - assert!(!vars.contains("lower")); - } -} diff --git a/baml_language/crates/sys_jinja_types/Cargo.toml b/baml_language/crates/sys_jinja_types/Cargo.toml deleted file mode 100644 index fa8bd553fd9..00000000000 --- a/baml_language/crates/sys_jinja_types/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "sys_jinja_types" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true - -[dependencies] -baml_base = { workspace = true } -indexmap = { workspace = true } -minijinja = { workspace = true } -strsim = { workspace = true } - -[lints] -workspace = true diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/expr.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/expr.rs deleted file mode 100644 index 21c70caeb5d..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/expr.rs +++ /dev/null @@ -1,930 +0,0 @@ -use std::str::FromStr; - -use baml_base::Literal as LiteralValue; -use indexmap::IndexMap; -use minijinja::machinery::ast; - -use super::{ - ScopeTracker, TypeError, - pretty_print::pretty_print, - types::{PredefinedTypes, Type}, -}; - -fn parse_as_function_call( - expr: &ast::Spanned, - state: &mut ScopeTracker, - types: &PredefinedTypes, - t: &Type, -) -> (Type, Vec) { - match t { - Type::FunctionRef(name) => { - let mut positional_args = Vec::new(); - let mut kwargs = IndexMap::new(); - for arg in &expr.args { - match arg { - ast::CallArg::Pos(expr) => { - let t = tracker_visit_expr(expr, state, types); - positional_args.push(t); - } - ast::CallArg::Kwarg(key, expr) => { - let t = tracker_visit_expr(expr, state, types); - kwargs.insert(*key, t); - } - ast::CallArg::PosSplat(_) | ast::CallArg::KwargSplat(_) => { - // For now, we'll handle splats as unknown - positional_args.push(Type::Unknown); - } - } - } - - types.check_function_args((name, expr), &positional_args, &kwargs) - } - Type::Both(x, y) => { - match (x.as_ref(), y.as_ref()) { - (Type::FunctionRef(_), Type::FunctionRef(_)) => {} - (Type::FunctionRef(_), _) => return parse_as_function_call(expr, state, types, x), - (_, Type::FunctionRef(_)) => return parse_as_function_call(expr, state, types, y), - _ => {} - } - - let (t1, e1) = parse_as_function_call(expr, state, types, x); - let (t2, e2) = parse_as_function_call(expr, state, types, y); - match (e1.is_empty(), e2.is_empty()) { - (true, true) => (Type::merge([t1, t2]), vec![]), - (true, false) => (t1, e1), - (false, true) => (t2, e2), - (false, false) => (Type::merge([t1, t2]), e1.into_iter().chain(e2).collect()), - } - } - Type::Union(items) => { - let items = items - .iter() - .map(|x| parse_as_function_call(expr, state, types, x)) - .reduce(|acc, x| { - let (t1, e1) = acc; - let (t2, e2) = x; - (Type::merge([t1, t2]), e1.into_iter().chain(e2).collect()) - }); - match items { - Some(x) => x, - None => ( - Type::Unknown, - vec![TypeError::new_invalid_type( - &expr.expr, - t, - "function", - expr.span(), - )], - ), - } - } - _ => ( - Type::Unknown, - vec![TypeError::new_invalid_type( - &expr.expr, - t, - "function", - expr.span(), - )], - ), - } -} - -// Helper function to check if binary operator is a comparison operator -fn is_comparison_op(op: &ast::BinOpKind) -> bool { - matches!( - op, - ast::BinOpKind::Eq - | ast::BinOpKind::Ne - | ast::BinOpKind::Lt - | ast::BinOpKind::Gt - | ast::BinOpKind::Lte - | ast::BinOpKind::Gte - ) -} - -// Helper function to check if union is nullable enum (enum + nullish only) -fn extract_enum_from_nullable_union(types: &[Type]) -> Option<&str> { - let mut enum_name: Option<&str> = None; - - for t in types { - match t { - Type::EnumValueRef(name) => { - if enum_name.is_some() { - // Multiple different enums in union - not a simple nullable enum - return None; - } - enum_name = Some(name); - } - Type::None | Type::Undefined => { - // Nullish types are allowed in nullable enums - continue; - } - _ => { - // Any other type (String, Int, etc.) means this isn't a nullable enum - return None; - } - } - } - - enum_name -} - -// Helper function to handle enum binary operations -fn handle_enum_binary_operation( - expr: &ast::Expr, - bin_expr: &ast::Spanned, - lhs: &Type, - rhs: &Type, - state: &mut ScopeTracker, - types: &PredefinedTypes, -) -> Option { - // First check for nullable enum patterns before strict enum handling - // Handle nullable enum to string literal comparisons - if let (Type::Union(union_types), Type::Literal(LiteralValue::String(str_val))) = (lhs, rhs) { - if let Some(enum_name) = extract_enum_from_nullable_union(union_types) { - if is_comparison_op(&bin_expr.op) { - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - enum_name, - str_val, - types, - expr.span(), - )); - return Some(Type::Bool); - } - } - } - if let (Type::Literal(LiteralValue::String(str_val)), Type::Union(union_types)) = (lhs, rhs) { - if let Some(enum_name) = extract_enum_from_nullable_union(union_types) { - if is_comparison_op(&bin_expr.op) { - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - enum_name, - str_val, - types, - expr.span(), - )); - return Some(Type::Bool); - } - } - } - - // Handle nullable enum vs generic string - if let (Type::Union(union_types), Type::String) = (lhs, rhs) { - if let Some(enum_name) = extract_enum_from_nullable_union(union_types) { - if is_comparison_op(&bin_expr.op) { - state.errors.push(TypeError::new_enum_string_cmp_deprecated( - expr, - enum_name, - expr.span(), - )); - return Some(Type::Bool); - } - } - } - if let (Type::String, Type::Union(union_types)) = (lhs, rhs) { - if let Some(enum_name) = extract_enum_from_nullable_union(union_types) { - if is_comparison_op(&bin_expr.op) { - state.errors.push(TypeError::new_enum_string_cmp_deprecated( - expr, - enum_name, - expr.span(), - )); - return Some(Type::Bool); - } - } - } - - // Handle nullable-to-nullable enum comparisons - if let (Type::Union(left_types), Type::Union(right_types)) = (lhs, rhs) { - let left_enum = extract_enum_from_nullable_union(left_types); - let right_enum = extract_enum_from_nullable_union(right_types); - - if let (Some(left), Some(right)) = (left_enum, right_enum) { - if is_comparison_op(&bin_expr.op) { - if left == right { - return Some(Type::Bool); - } else { - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - left, - "different_enum", - types, - expr.span(), - )); - return Some(Type::Bool); - } - } - } - } - - // Now check if either operand is an EnumValueRef for strict handling - match (lhs, rhs) { - // Both are EnumValueRef - only allow comparison ops between same enum - (Type::EnumValueRef(e1), Type::EnumValueRef(e2)) => { - match &bin_expr.op { - op if is_comparison_op(op) => { - if e1 == e2 { - Some(Type::Bool) - } else { - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - e1, - "different_enum", - types, - expr.span(), - )); - Some(Type::Unknown) - } - } - _ => { - // Disallow arithmetic/string ops on enums - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - e1, - "arithmetic_operation", - types, - expr.span(), - )); - Some(Type::Unknown) - } - } - } - // EnumValueRef with string literal - suggest proper enum syntax - (Type::EnumValueRef(enum_name), Type::Literal(LiteralValue::String(str_val))) - | (Type::Literal(LiteralValue::String(str_val)), Type::EnumValueRef(enum_name)) => { - match &bin_expr.op { - op if is_comparison_op(op) => { - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - enum_name, - str_val, - types, - expr.span(), - )); - Some(Type::Bool) - } - _ => { - // Disallow arithmetic/string ops on enums - state.errors.push(TypeError::new_enum_literal_suggestion( - expr, - enum_name, - str_val, - types, - expr.span(), - )); - Some(Type::Unknown) - } - } - } - // EnumValueRef with generic string - placeholder message - (Type::EnumValueRef(enum_name), Type::String) - | (Type::String, Type::EnumValueRef(enum_name)) => { - match &bin_expr.op { - op if is_comparison_op(op) => { - state.errors.push(TypeError::new_enum_string_cmp_deprecated( - expr, - enum_name, - expr.span(), - )); - Some(Type::Bool) - } - _ => { - // Disallow arithmetic/string ops on enums - state.errors.push(TypeError::new_enum_string_cmp_deprecated( - expr, - enum_name, - expr.span(), - )); - Some(Type::Unknown) - } - } - } - // Any other combination with EnumValueRef is invalid - (Type::EnumValueRef(enum_name), _) | (_, Type::EnumValueRef(enum_name)) => { - state.errors.push(TypeError::new_enum_string_cmp_deprecated( - expr, - enum_name, - expr.span(), - )); - Some(Type::Unknown) - } - // No enums involved - return None to fall through to normal operator handling - _ => None, - } -} - -fn tracker_visit_expr( - expr: &ast::Expr<'_>, - state: &mut ScopeTracker, - types: &PredefinedTypes, -) -> Type { - match expr { - ast::Expr::Var(var) if var.id == "null" => Type::None, - ast::Expr::Var(var) => match types.resolve(var.id) { - Some(t) => t, - None => { - state.errors.push(TypeError::new_unresolved_variable( - var.id, - var.span(), - types.variable_names(), - )); - Type::Unknown - } - }, - ast::Expr::Const(c) => infer_const_type(&c.value), - ast::Expr::UnaryOp(expr) => { - let expected = match expr.op { - ast::UnaryOpKind::Not => Type::Bool, - ast::UnaryOpKind::Neg => Type::Number, - }; - - let _inner = tracker_visit_expr(&expr.expr, state, types); - // TODO: Check for type compatibility - - expected - } - ast::Expr::BinOp(bin_expr) => { - let lhs = tracker_visit_expr(&bin_expr.left, state, types); - let rhs = tracker_visit_expr(&bin_expr.right, state, types); - - // Handle enum operations with the helper function - if let Some(result) = - handle_enum_binary_operation(expr, bin_expr, &lhs, &rhs, state, types) - { - return result; - } - - // No enums involved - fall through to normal operator handling - match bin_expr.op { - ast::BinOpKind::Add => { - if lhs.is_subtype_of(&Type::String) || rhs.is_subtype_of(&Type::String) { - Type::String - } else { - Type::Number - } - } - ast::BinOpKind::Sub => Type::Number, - ast::BinOpKind::Mul => Type::Number, - ast::BinOpKind::Div => Type::Number, - ast::BinOpKind::Pow => Type::Number, - ast::BinOpKind::FloorDiv => Type::Number, - ast::BinOpKind::Rem => Type::Number, - ast::BinOpKind::Eq => Type::Bool, - ast::BinOpKind::Ne => Type::Bool, - ast::BinOpKind::Lt => Type::Bool, - ast::BinOpKind::Gt => Type::Bool, - ast::BinOpKind::Lte => Type::Bool, - ast::BinOpKind::Gte => Type::Bool, - ast::BinOpKind::In => Type::Bool, - ast::BinOpKind::Concat => Type::String, - ast::BinOpKind::ScAnd => Type::Bool, - ast::BinOpKind::ScOr => Type::Bool, - } - } - ast::Expr::IfExpr(expr) => { - let _test = tracker_visit_expr(&expr.test_expr, state, types); - - let true_expr = tracker_visit_expr(&expr.true_expr, state, types); - let false_expr = expr - .false_expr - .as_ref() - .map(|x| tracker_visit_expr(x, state, types)) - .unwrap_or(Type::Unknown); - Type::merge([true_expr, false_expr]) - } - ast::Expr::Filter(expr) => { - // Filters have a name - let inner = tracker_visit_expr(expr.expr.as_ref().unwrap(), state, types); - - let mut ensure_type = |error_string: &str| { - state.errors.push(TypeError::new_invalid_type( - expr.expr.as_ref().unwrap(), - &inner, - error_string, - expr.span(), - )); - }; - - let valid_filters = vec![ - "abs", - "attrs", - "batch", - "bool", - "capitalize", - "escape", - "first", - "last", - "default", - "float", - "indent", - "int", - "dictsort", - "items", - "join", - "length", - "list", - "lower", - "upper", - "map", - "max", - "min", - "pprint", - "regex_match", - "reject", - "rejectattr", - "replace", - "reverse", - "round", - "safe", - "select", - "selectattr", - "slice", - "sort", - "split", - "sum", - "title", - "tojson", - "json", - "format", - "trim", - "unique", - "urlencode", - ]; - match expr.name { - "abs" => { - if !inner.is_subtype_of(&Type::Number) { - ensure_type("number"); - } - Type::Number - } - "attrs" => Type::Unknown, - "batch" => Type::Unknown, - "bool" => Type::Bool, - "capitalize" | "escape" => { - if !inner.is_subtype_of(&Type::String) { - ensure_type("string"); - } - Type::String - } - "first" | "last" => match inner { - Type::List(t) => Type::merge([*t, Type::None]), - Type::Unknown => Type::Unknown, - _ => { - ensure_type("list"); - Type::Unknown - } - }, - "default" => Type::Unknown, - "float" => Type::Float, - "indent" => Type::String, - "int" => Type::Int, - "dictsort" | "items" => match inner { - Type::Map(k, v) => Type::List(Box::new(Type::Tuple(vec![*k, *v]))), - Type::ClassRef(_) => { - Type::List(Box::new(Type::Tuple(vec![Type::String, Type::Unknown]))) - } - _ => { - ensure_type("map or class"); - Type::Unknown - } - }, - "join" => Type::String, - "length" => match inner { - Type::List(_) | Type::String | Type::ClassRef(_) | Type::Map(_, _) => Type::Int, - Type::Unknown => Type::Unknown, - _ => { - ensure_type("list, string, class or map"); - Type::Unknown - } - }, - "list" => Type::List(Box::new(Type::Unknown)), - "lower" | "upper" => { - if !inner.is_subtype_of(&Type::String) { - ensure_type("string"); - } - Type::String - } - "map" => Type::Unknown, - "max" => Type::Unknown, - "min" => Type::Unknown, - "pprint" => Type::Unknown, - "regex_match" => Type::Bool, - "reject" => Type::Unknown, - "rejectattr" => Type::Unknown, - "replace" => Type::String, - "reverse" => Type::Unknown, - "round" => Type::Float, - "safe" => Type::String, - "select" => Type::Unknown, - "selectattr" => Type::Unknown, - "slice" => Type::Unknown, - "sort" => Type::Unknown, - "split" => Type::List(Box::new(Type::String)), - "sum" => match inner.clone() { - Type::List(elem_type) => { - if elem_type.is_subtype_of(&Type::Float) { - Type::Float - } else if elem_type.is_subtype_of(&Type::Int) { - Type::Int - } else { - ensure_type("(int|float)[]"); - Type::String - } - } - _ => { - ensure_type("(int|float)[]"); - Type::Bool - } - }, - "title" => Type::String, - "tojson" | "json" => Type::String, - "format" => Type::String, - "trim" => Type::String, - "unique" => Type::Unknown, - "urlencode" => Type::String, - other => { - state.errors.push(TypeError::new_invalid_filter( - other, - expr.span(), - &valid_filters, - )); - Type::Unknown - } - } - } - ast::Expr::Test(expr) => { - let _test = tracker_visit_expr(&expr.expr, state, types); - // TODO: Check for type compatibility - Type::Bool - } - ast::Expr::GetAttr(expr) => { - let parent = tracker_visit_expr(&expr.expr, state, types); - - match &parent { - Type::ClassRef(c) => { - let (t, err) = types.check_class_property( - &pretty_print(&expr.expr), - c, - expr.name, - expr.span(), - ); - if let Some(e) = err { - state.errors.push(e); - } - t - } - Type::EnumTypeRef(e) => { - let (t, err) = types.check_enum_property( - &pretty_print(&expr.expr), - e, - expr.name, - expr.span(), - ); - if let Some(e) = err { - state.errors.push(e); - } - t - } - Type::EnumValueRef(enum_value) => match expr.name { - "value" => Type::String, - _ => { - state.errors.push(TypeError::new_enum_value_property_error( - &pretty_print(&expr.expr), - enum_value, - expr.name, - expr.span(), - )); - Type::Unknown - } - }, - Type::Union(_) | Type::Alias { .. } => { - typecheck_attr_access_on_union(&parent, expr, types, state) - } - Type::Unknown => Type::Unknown, - other => expected_class_got(other, expr, state), - } - } - ast::Expr::GetItem(_expr) => Type::Unknown, - ast::Expr::Slice(_slice) => Type::Unknown, - ast::Expr::Call(expr) => { - // Intercept string method calls: "str".method(args) or string_var.method(args) - if let ast::Expr::GetAttr(get_attr) = &expr.expr { - let parent = tracker_visit_expr(&get_attr.expr, state, types); - if parent.is_subtype_of(&Type::String) { - if let Some(ret) = string_method_return_type(get_attr.name) { - // Visit args for side effects (variable tracking, error checking) - for arg in &expr.args { - match arg { - ast::CallArg::Pos(e) | ast::CallArg::Kwarg(_, e) => { - tracker_visit_expr(e, state, types); - } - ast::CallArg::PosSplat(e) | ast::CallArg::KwargSplat(e) => { - tracker_visit_expr(e, state, types); - } - } - } - return ret; - } - } - } - let func = tracker_visit_expr(&expr.expr, state, types); - let (t, errs) = parse_as_function_call(expr, state, types, &func); - state.errors.extend(errs); - t - } - ast::Expr::List(expr) => { - let inner = Type::merge( - expr.items - .iter() - .map(|x| tracker_visit_expr(x, state, types)), - ); - Type::List(Box::new(inner)) - } - ast::Expr::Map(expr) => { - let keys = Type::merge( - expr.keys - .iter() - .map(|x| tracker_visit_expr(x, state, types)), - ); - let values = Type::merge( - expr.values - .iter() - .map(|x| tracker_visit_expr(x, state, types)), - ); - Type::Map(Box::new(keys), Box::new(values)) - } - } -} - -fn infer_const_type(v: &minijinja::value::Value) -> Type { - match v.kind() { - minijinja::value::ValueKind::Undefined => Type::Undefined, - minijinja::value::ValueKind::None => Type::None, - minijinja::value::ValueKind::Bool => match bool::from_str(&v.to_string()) { - Ok(b) => Type::Literal(LiteralValue::Bool(b)), - Err(_) => Type::Bool, - }, - minijinja::value::ValueKind::String => Type::Literal(LiteralValue::String(v.to_string())), - minijinja::value::ValueKind::Seq => match v.len() { - Some(0) => Type::List(Box::new(Type::Unknown)), - Some(_) => { - if let Ok(iter) = v.try_iter() { - let inner = iter - .map(|x| infer_const_type(&x)) - .fold(None, |acc, x| match acc { - None => Some(x), - Some(Type::Union(acc)) => { - let t = Type::Union(acc); - if x.is_subtype_of(&t) { - Some(t) - } else if let Type::Union(mut acc) = t { - acc.push(x); - Some(Type::Union(acc)) - } else { - unreachable!("minijinja") - } - } - Some(acc) => { - if x.is_subtype_of(&acc) { - Some(acc) - } else { - Some(Type::Union(vec![acc, x])) - } - } - }) - .unwrap_or(Type::Unknown); - Type::List(Box::new(inner)) - } else { - Type::List(Box::new(Type::Unknown)) - } - } - None => Type::List(Box::new(Type::Unknown)), - }, - minijinja::value::ValueKind::Map => Type::Unknown, - // We don't handle these types - minijinja::value::ValueKind::Number => match i64::from_str(&v.to_string()) { - Ok(i) => Type::Literal(LiteralValue::Int(i)), - Err(_) => Type::Number, - }, - minijinja::value::ValueKind::Bytes => Type::Undefined, - minijinja::value::ValueKind::Iterable => Type::Unknown, - minijinja::value::ValueKind::Plain => Type::Unknown, - minijinja::value::ValueKind::Invalid => Type::Unknown, - _ => Type::Unknown, - } -} - -pub(crate) fn evaluate_type( - expr: &ast::Expr, - types: &PredefinedTypes, -) -> Result> { - let mut state = ScopeTracker::new(); - // Lint: bare function reference without call, e.g. `{{ MyTemplateString }}` vs `{{ MyTemplateString() }}` - if let ast::Expr::Var(var) = expr { - if let Some((_, _)) = types.as_function(var.id) { - state - .errors - .push(TypeError::new_function_reference_without_call( - var.id, - var.span(), - )); - } - } - let result = tracker_visit_expr(expr, &mut state, types); - - if state.errors.is_empty() { - Ok(result) - } else { - Err(state.errors) - } -} - -/// Verifies that an attribute is present in all items of a union. -/// -/// This is used especially for if statements like `if v.kind == "X"` where v -/// is a union of types and we need to check that `kind` is present in all of -/// the types, thus making the attr access valid in every case, therefore not -/// a type error. -/// -/// This functions returns the type of the attr if present in all items. -/// Otherwise, it returns [`Type::Unknown`] and pushes a type error to the -/// `state` param. -/// -/// TODO: This function is very similar to `narrow_attr_access_on_union_var` in -/// `stmt.rs`. Reusing the code is not straightforward though (at least if we -/// want it to be readable), but we should try something because this is kind of -/// error prone if we add more types that need to be covered. -fn typecheck_attr_access_on_union( - union_type: &Type, - get_attr: &ast::Spanned>, - types: &PredefinedTypes, - state: &mut ScopeTracker, -) -> Type { - // Extract union name if this is a type alias - let union_name = match union_type { - Type::Alias { name, .. } => Some(name.as_str()), - _ => None, - }; - - // Resolve items. - let union_items = match union_type { - Type::Union(items) => items, - Type::Alias { resolved, .. } => match resolved.as_ref() { - Type::Union(items) => items, - _ => return expected_class_got(union_type, get_attr, state), - }, - _ => { - return expected_class_got(union_type, get_attr, state); - } - }; - - // Attribute must be present on all items of the union and also have the - // same type. - let mut attr_type = None; - let mut classes_missing_property: Vec<&str> = Vec::new(); - let mut has_type_mismatch = false; - - // Search recursively for all types in the union to check - // if they all contain the property. - let mut stack = Vec::from_iter(union_items.iter()); - - while let Some(union_item_type) = stack.pop() { - match union_item_type { - Type::ClassRef(class_name) => { - // Get type of prop - let (class_prop_type, err) = types.check_class_property( - &pretty_print(&get_attr.expr), - class_name, - get_attr.name, - get_attr.span(), - ); - - // Prop not found in one of the types - track it - if err.is_some() { - classes_missing_property.push(class_name); - continue; - } - - // Check if previous type matches the current one - match &attr_type { - None => attr_type = Some(class_prop_type), - - Some(prev_type) => { - // Found two distinct types for the same prop. - if !class_prop_type.equals_ignoring_literal_values(prev_type) { - has_type_mismatch = true; - } - } - } - } - - // Resolve aliases. - Type::Alias { resolved, .. } => stack.push(resolved), - - // Recurse into nested unions - Type::Union(nested) => stack.extend(nested.iter()), - - // Found a type that's not a class, stop here. - _ => { - let variable_name = pretty_print(&get_attr.expr); - state.errors.push(TypeError::new_non_class_in_union( - &variable_name, - get_attr.name, - &union_item_type.name(), - get_attr.span(), - )); - return Type::Unknown; - } - } - } - - // Report specific errors based on what went wrong - if !classes_missing_property.is_empty() { - let variable_name = pretty_print(&get_attr.expr); - state - .errors - .push(TypeError::new_property_not_found_in_union( - &variable_name, - get_attr.name, - &classes_missing_property, - union_name, - get_attr.span(), - )); - return Type::Unknown; - } - - if has_type_mismatch { - let variable_name = pretty_print(&get_attr.expr); - state - .errors - .push(TypeError::new_property_type_mismatch_in_union( - &variable_name, - get_attr.name, - union_name, - get_attr.span(), - )); - return Type::Unknown; - } - - match attr_type { - Some(attr_type) => attr_type, - None => expected_class_got(union_type, get_attr, state), - } -} - -/// Return type for a known Python string method, or None if unknown. -/// -/// Source of truth for supported methods: -/// https://docs.rs/minijinja-contrib/latest/src/minijinja_contrib/pycompat.rs.html -fn string_method_return_type(method: &str) -> Option { - let methods: IndexMap<&str, Type> = IndexMap::from([ - // Methods returning String - ("capitalize", Type::String), - ("format", Type::String), - ("join", Type::String), - ("lower", Type::String), - ("lstrip", Type::String), - ("replace", Type::String), - ("rstrip", Type::String), - ("strip", Type::String), - ("title", Type::String), - ("upper", Type::String), - // Methods returning Bool - ("endswith", Type::Bool), - ("isalnum", Type::Bool), - ("isalpha", Type::Bool), - ("isascii", Type::Bool), - ("isdigit", Type::Bool), - ("islower", Type::Bool), - ("isnumeric", Type::Bool), - ("isupper", Type::Bool), - ("startswith", Type::Bool), - // Methods returning Int - ("count", Type::Int), - ("find", Type::Int), - ("rfind", Type::Int), - // Methods returning List[String] - ("split", Type::List(Box::new(Type::String))), - ("splitlines", Type::List(Box::new(Type::String))), - ]); - methods.get(method).cloned() -} - -/// Helper for [`typecheck_attr_access_on_union`]. -/// Used when the type is not a union at all (e.g., primitive type alias). -fn expected_class_got( - got: &Type, - get_attr: &ast::Spanned>, - state: &mut ScopeTracker, -) -> Type { - state.errors.push(TypeError::new_invalid_type( - &get_attr.expr, - got, - "class", - get_attr.span(), - )); - - Type::Unknown -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/mod.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/mod.rs deleted file mode 100644 index 3502b4cfb01..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/mod.rs +++ /dev/null @@ -1,465 +0,0 @@ -mod expr; -mod pretty_print; -mod stmt; -#[cfg(test)] -mod test_expr; -#[cfg(test)] -mod test_stmt; -mod types; - -use std::{collections::HashSet, fmt::Debug, ops::Index}; - -use indexmap::{IndexMap, IndexSet}; -use minijinja::machinery::{Span, ast::Expr}; - -pub(crate) use self::stmt::get_variable_types; -pub use self::types::{EnumDefinition, EnumValueDefinition, JinjaContext, PredefinedTypes, Type}; - -#[derive(Debug, Clone, PartialEq)] -pub struct TypeError { - message: String, - span: Span, -} - -// Implementing the Display trait for TypeError. -impl std::fmt::Display for TypeError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{} at {:?}", self.message, self.span) - } -} - -// Implementing the Error trait for TypeError. -impl std::error::Error for TypeError {} - -fn sort_by_match<'a, I, T>(name: &str, options: &'a I, max_return: Option) -> Vec<&'a str> -where - I: Index + 'a, - &'a I: IntoIterator, - T: AsRef + 'a, -{ - // The maximum allowed distance for a string to be considered similar. - const THRESHOLD: usize = 20; - - // Calculate distances and sort names by distance - let mut name_distances = options - .into_iter() - .enumerate() - .map(|(idx, n)| { - ( - // Case insensitive comparison - strsim::osa_distance(&n.as_ref().to_lowercase(), &name.to_lowercase()), - idx, - ) - }) - .collect::>(); - - name_distances.sort_by_key(|k| k.0); - - // Filter names based on the threshold - let filtered_names = name_distances - .iter() - .filter(|&&(dist, _)| dist <= THRESHOLD) - .map(|&(_, idx)| options.index(idx).as_ref()); - - // Return either a limited or full set of filtered names - match max_return { - Some(max) => filtered_names.take(max).collect(), - None => filtered_names.collect(), - } -} - -impl TypeError { - pub fn message(&self) -> &str { - &self.message - } - - pub fn span(&self) -> Span { - self.span - } - - fn new_unresolved_variable(name: &str, span: Span, options: Vec) -> Self { - let mut close_names = sort_by_match(name, &options, Some(3)); - close_names.sort(); - let close_names = close_names; - - let message = if close_names.is_empty() { - // If no names are close enough, suggest nothing or provide a generic message - format!("Variable `{name}` does not exist.") - } else if close_names.len() == 1 { - // If there's only one close name, suggest it - format!( - "Variable `{}` does not exist. Did you mean `{}`?", - name, close_names[0] - ) - } else { - // If there are multiple close names, suggest them all - let suggestions = close_names.join("`, `"); - format!("Variable `{name}` does not exist. Did you mean one of these: `{suggestions}`?") - }; - - Self { message, span } - } - - fn new_wrong_arg_type( - func: &str, - span: Span, - name: &str, - _arg_span: Span, - expected: Type, - got: Type, - ) -> Self { - Self { - message: format!( - "Function '{}' expects argument '{}' to be of type {}, but got {}", - func, - name, - expected.name(), - got.name() - ), - span, - } - } - - fn new_missing_arg(func: &str, span: Span, name: &str) -> Self { - Self { - message: format!("Function '{func}' expects argument '{name}'"), - span, - } - } - - fn new_wrong_arg_count(func: &str, span: Span, expected: usize, got: usize) -> Self { - Self { - message: format!("Function '{func}' expects {expected} arguments, but got {got}"), - span, - } - } - - // TODO: There's a bug with the suggestions, they are not consistent due to - // either some ordering issue or closest match algorithm does weird stuff - // and returns results non-deterministically. See commented test in - // baml-lib/jinja/src/evaluate_type/test_expr.rs - // NB(sam): this is probably because valid_args is a HashSet, not an IndexSet - fn new_unknown_arg(func: &str, span: Span, name: &str, valid_args: HashSet<&String>) -> Self { - let names = valid_args.into_iter().collect::>(); - let mut close_names = sort_by_match(name, &names, Some(3)); - close_names.sort(); - let close_names = close_names; - - let message = if close_names.is_empty() { - // If no names are close enough, suggest nothing or provide a generic message - format!("Function '{func}' does not have an argument '{name}'.") - } else if close_names.len() == 1 { - // If there's only one close name, suggest it - format!( - "Function '{}' does not have an argument '{}'. Did you mean '{}'?", - func, name, close_names[0] - ) - } else { - // If there are multiple close names, suggest them all - let suggestions = close_names.join("', '"); - format!( - "Function '{func}' does not have an argument '{name}'. Did you mean one of these: '{suggestions}'?" - ) - }; - - Self { message, span } - } - - fn new_invalid_filter(name: &str, span: Span, valid_filters: &Vec<&str>) -> Self { - let mut close_names = sort_by_match(name, valid_filters, Some(5)); - close_names.sort(); - let close_names = close_names; - - let message = if close_names.is_empty() { - // If no names are close enough, suggest nothing or provide a generic message - format!("Filter '{name}' does not exist") - } else if close_names.len() == 1 { - // If there's only one close name, suggest it - format!( - "Filter '{}' does not exist. Did you mean '{}'?", - name, close_names[0] - ) - } else { - // If there are multiple close names, suggest them all - let suggestions = close_names.join("', '"); - format!("Filter '{name}' does not exist. Did you mean one of these: '{suggestions}'?") - }; - - Self { - message: format!( - "{message}\n\nSee: https://docs.rs/minijinja/latest/minijinja/filters/index.html#functions for the compelete list" - ), - span, - } - } - - pub fn new_function_reference_without_call(func: &str, span: Span) -> Self { - Self { - message: format!( - "Function '{func}' referenced without parentheses. Did you mean '{func}()'?" - ), - span, - } - } - - fn new_enum_literal_suggestion( - expr: &Expr, - enum_name: &str, - literal_value: &str, - types: &types::PredefinedTypes, - span: Span, - ) -> Self { - let enum_def = match types.as_enum(enum_name) { - Some(def) => def, - None => return Self::new_enum_string_cmp_deprecated(expr, enum_name, span), - }; - - // 1. EXACT VALUE NAME MATCH - if enum_def.values.iter().any(|v| v.name == literal_value) { - return Self { - message: format!( - "Use `{enum_name}.{literal_value}` instead of \"{literal_value}\" - comparing enums with strings will soon be deprecated." - ), - span, - }; - } - - // 2. CASE-INSENSITIVE VALUE NAME MATCH - if let Some(correct_case) = enum_def - .values - .iter() - .find(|v| v.name.to_lowercase() == literal_value.to_lowercase()) - { - return Self { - message: format!( - "Use `{}.{}` instead of \"{}\" - comparing enums with strings will soon be deprecated.", - enum_name, correct_case.name, literal_value - ), - span, - }; - } - - // 3. EXACT ALIAS MATCH - if let Some(value_for_alias) = enum_def - .values - .iter() - .find(|v| v.alias.as_ref() == Some(&literal_value.to_string())) - { - return Self { - message: format!( - "Did you mean `{}.{}` instead of \"{}\" (alias)? Enums are not equal to their alias values.", - enum_name, value_for_alias.name, literal_value - ), - span, - }; - } - - // 4. CASE-INSENSITIVE ALIAS MATCH - if let Some(value_for_alias) = enum_def.values.iter().find(|v| { - v.alias.as_ref().map(|a| a.to_lowercase()) == Some(literal_value.to_lowercase()) - }) { - return Self { - message: format!( - "Did you mean `{}.{}` instead of \"{}\" (alias)? Enums are not equal to their alias values.", - enum_name, value_for_alias.name, literal_value - ), - span, - }; - } - - // 5. FUZZY MATCH using existing sort_by_match function - let mut all_searchable_terms = Vec::new(); - let mut term_to_value_name = IndexMap::new(); - for value in &enum_def.values { - all_searchable_terms.push(value.name.clone()); - term_to_value_name.insert(value.name.clone(), value.name.clone()); - - if let Some(alias) = &value.alias { - all_searchable_terms.push(alias.clone()); - term_to_value_name.insert(alias.clone(), value.name.clone()); - } - } - - let close_matches = sort_by_match(literal_value, &all_searchable_terms, Some(3)); - if !close_matches.is_empty() { - let unique_values: IndexSet<_> = close_matches - .iter() - .filter_map(|term| term_to_value_name.get(*term)) - .collect(); - let suggestions: Vec<_> = unique_values - .iter() - .map(|v| format!("{enum_name}.{v}")) - .collect(); - - return Self { - message: if suggestions.len() == 1 { - format!( - "Use `{}` instead of \"{}\" - comparing enums with strings will soon be deprecated.", - suggestions[0], literal_value - ) - } else { - format!( - "Use one of: {} - comparing enums with strings will soon be deprecated.", - suggestions.join(", ") - ) - }, - span, - }; - } - - // 6. FALLBACK: Show all available values - Self { - message: format!( - "Use one of: {} - comparing enums with strings will soon be deprecated.", - enum_def - .values - .iter() - .map(|v| format!("{}.{}", enum_name, v.name)) - .collect::>() - .join(", ") - ), - span, - } - } - - fn new_enum_string_cmp_deprecated(_expr: &Expr, enum_name: &str, span: Span) -> Self { - Self { - message: format!( - "Comparing enum {enum_name} to string variable - enum-string comparisons will soon be deprecated. Please see https://github.com/BoundaryML/baml/issues/2339." - ), - span, - } - } - - fn new_enum_value_property_error( - variable_name: &str, - enum_value: &str, - property: &str, - span: Span, - ) -> Self { - Self { - message: format!( - "enum value {enum_value} ({variable_name}) does not have a property '{property}'" - ), - span, - } - } - - fn new_invalid_type(expr: &Expr, got: &Type, expected: &str, span: Span) -> Self { - Self { - message: format!( - "'{}' is {}, expected {}", - pretty_print::pretty_print(expr), - if got.is_subtype_of(&Type::Undefined) { - "undefined".to_string() - } else { - format!("a {}", got.name()) - }, - expected - ), - span, - } - } - - #[allow(dead_code)] - fn new_dot_operator_not_supported( - name: &str, - r#type: &Type, - property: &str, - span: Span, - ) -> Self { - Self { - message: format!( - "'{}' ({}) does not have a property '{}'", - name, - r#type.name(), - property - ), - span, - } - } - - fn new_property_not_defined( - variable_name: &str, - class_name: &str, - property: &str, - span: Span, - ) -> Self { - Self { - message: format!( - "class {class_name} ({variable_name}) does not have a property '{property}'" - ), - span, - } - } - - fn new_class_not_defined(class: &str) -> Self { - Self { - message: format!("Class '{class}' is not defined"), - span: Span::default(), - } - } - - fn new_enum_not_defined(class: &str) -> Self { - Self { - message: format!("Class '{class}' is not defined"), - span: Span::default(), - } - } - - fn new_property_not_found_in_union( - _variable_name: &str, - property: &str, - missing_on_classes: &[&str], - union_name: Option<&str>, - span: Span, - ) -> Self { - let classes_str = missing_on_classes.join(", "); - let message = match union_name { - Some(name) => format!( - "property '{property}' does not exist on {classes_str} in type alias {name}" - ), - None => format!("property '{property}' does not exist on {classes_str}"), - }; - Self { message, span } - } - - fn new_property_type_mismatch_in_union( - _variable_name: &str, - property: &str, - union_name: Option<&str>, - span: Span, - ) -> Self { - let message = match union_name { - Some(name) => format!( - "property '{property}' has inconsistent types across classes in type alias {name}" - ), - None => format!("property '{property}' has inconsistent types across union members"), - }; - Self { message, span } - } - - fn new_non_class_in_union( - variable_name: &str, - property: &str, - non_class_type: &str, - span: Span, - ) -> Self { - Self { - message: format!( - "cannot access property '{property}' on '{variable_name}': union contains non-class type {non_class_type}" - ), - span, - } - } -} - -struct ScopeTracker { - errors: Vec, -} - -impl ScopeTracker { - fn new() -> Self { - Self { errors: Vec::new() } - } -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/pretty_print.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/pretty_print.rs deleted file mode 100644 index 3be62f4dd1b..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/pretty_print.rs +++ /dev/null @@ -1,153 +0,0 @@ -use minijinja::machinery::ast::{self, CallArg, Expr}; - -fn pretty_print_call_arg(arg: &CallArg) -> String { - match arg { - CallArg::Pos(expr) => pretty_print(expr), - CallArg::Kwarg(key, expr) => format!("{}={}", key, pretty_print(expr)), - CallArg::PosSplat(expr) => format!("*{}", pretty_print(expr)), - CallArg::KwargSplat(expr) => format!("**{}", pretty_print(expr)), - } -} - -pub(super) fn pretty_print(expr: &Expr) -> String { - match expr { - Expr::Var(v) => v.id.to_string(), - Expr::Const(c) => c.value.to_string(), - Expr::Slice(c) => format!( - "({})[{}:{}:{}]", - pretty_print(&c.expr), - c.start.as_ref().map(pretty_print).unwrap_or("".into()), - c.stop.as_ref().map(pretty_print).unwrap_or("".into()), - c.step.as_ref().map(pretty_print).unwrap_or("".into()) - ), - Expr::UnaryOp(op) => { - format!( - "{}{}", - match op.op { - ast::UnaryOpKind::Not => "not ", - ast::UnaryOpKind::Neg => "-", - }, - pretty_print(&op.expr) - ) - } - Expr::BinOp(op) => { - format!( - "({} {} {})", - pretty_print(&op.left), - match op.op { - ast::BinOpKind::Add => "+", - ast::BinOpKind::Sub => "-", - ast::BinOpKind::Mul => "*", - ast::BinOpKind::Div => "/", - ast::BinOpKind::Pow => "**", - ast::BinOpKind::FloorDiv => "//", - ast::BinOpKind::Rem => "%", - ast::BinOpKind::Eq => "==", - ast::BinOpKind::Ne => "!=", - ast::BinOpKind::Lt => "<", - ast::BinOpKind::Gt => ">", - ast::BinOpKind::In => "in", - ast::BinOpKind::Lte => "<=", - ast::BinOpKind::Gte => ">=", - ast::BinOpKind::Concat => "~", - ast::BinOpKind::ScAnd => "and", - ast::BinOpKind::ScOr => "or", - }, - pretty_print(&op.right) - ) - } - Expr::IfExpr(expr) => { - format!( - "{true_expr} if {test}{false_expr}", - test = pretty_print(&expr.test_expr), - true_expr = pretty_print(&expr.true_expr), - false_expr = expr - .false_expr - .as_ref() - .map(|x| format!(" else {}", pretty_print(x))) - .unwrap_or("".into()) - ) - } - Expr::Filter(expr) => { - format!( - "{}|{}{}", - expr.expr - .as_ref() - .map(|x| pretty_print(x)) - .unwrap_or("".into()), - expr.name, - match expr.args.len() { - 0 => "".into(), - _ => format!( - "({})", - expr.args - .iter() - .map(pretty_print_call_arg) - .collect::>() - .join(",") - ), - } - ) - } - Expr::Test(expr) => { - format!( - "{} is {}{}", - pretty_print(&expr.expr), - expr.name, - match expr.args.len() { - 0 => "".into(), - _ => format!( - "({})", - expr.args - .iter() - .map(pretty_print_call_arg) - .collect::>() - .join(",") - ), - } - ) - } - Expr::GetAttr(attr) => { - format!("{}.{}", pretty_print(&attr.expr), attr.name) - } - Expr::GetItem(expr) => { - format!( - "{}[{}]", - pretty_print(&expr.expr), - pretty_print(&expr.subscript_expr) - ) - } - Expr::Call(expr) => { - format!( - "{}({})", - pretty_print(&expr.expr), - expr.args - .iter() - .map(pretty_print_call_arg) - .collect::>() - .join(",") - ) - } - Expr::List(expr) => { - format!( - "[{}]", - expr.items - .iter() - .map(pretty_print) - .collect::>() - .join(",") - ) - } - Expr::Map(expr) => { - format!( - "{{{}}}", - expr.keys - .iter() - .zip(expr.values.iter()) - .map(|(k, v)| format!("{}:{}", pretty_print(k), pretty_print(v))) - .collect::>() - .join(",") - ) - } - } -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/stmt.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/stmt.rs deleted file mode 100644 index f4eb92fc1a4..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/stmt.rs +++ /dev/null @@ -1,516 +0,0 @@ -use baml_base::Literal as LiteralValue; -use minijinja::machinery::ast::{self, Spanned, Stmt, UnaryOpKind}; - -use super::{TypeError, expr::evaluate_type, types::PredefinedTypes}; -use crate::evaluate_type::types::Type; - -fn track_walk(node: &ast::Stmt<'_>, state: &mut PredefinedTypes) { - match node { - ast::Stmt::Template(stmt) => { - state.start_scope(); - stmt.children.iter().for_each(|x| track_walk(x, state)); - state.end_scope(); - } - ast::Stmt::EmitExpr(expr) => { - let expr_type = evaluate_type(&expr.expr, state); - if expr_type.is_err() { - state.errors_mut().extend(expr_type.err().unwrap()); - } - } - ast::Stmt::EmitRaw(_) => {} - ast::Stmt::ForLoop(stmt) => { - let iter_type = match evaluate_type(&stmt.iter, state) { - Ok(t) => match t { - Type::List(t) => *t, - Type::Map(k, _) => *k, - _ => Type::Unknown, - }, - Err(e) => { - state.errors_mut().extend(e); - Type::Unknown - } - }; - - let _filter_type = stmt.filter_expr.as_ref().map(|x| evaluate_type(x, state)); - - state.start_scope(); - match &stmt.target { - ast::Expr::Var(var) => state.add_variable(var.id, iter_type), - ast::Expr::List(list) => match iter_type { - Type::List(t) => { - list.items.iter().for_each(|x| { - if let ast::Expr::Var(var) = x { - state.add_variable(var.id, *t.clone()); - } - }); - } - Type::Tuple(items) => { - if list.items.len() != items.len() { - state.errors_mut().push(TypeError { - message: format!("Expected {} items", items.len()), - span: list.span(), - }); - list.items.iter().for_each(|x| { - if let ast::Expr::Var(var) = x { - state.add_variable(var.id, Type::Unknown); - } - }); - } else { - list.items.iter().zip(items.iter()).for_each(|(x, t)| { - if let ast::Expr::Var(var) = x { - state.add_variable(var.id, t.clone()); - } else { - state.errors_mut().push(TypeError { - message: "Expected variable".to_string(), - span: list.span(), - }); - } - }); - } - } - _ => {} - }, - _ => { - state.errors_mut().push(TypeError { - message: "Not a sequence".to_string(), - span: stmt.span(), - }); - } - } - - // We need to set some variables here - - state.start_scope(); - state.add_variable("loop", Type::ClassRef("jinja::loop".into())); - stmt.body.iter().for_each(|x| track_walk(x, state)); - state.end_scope(); - state.start_scope(); - stmt.else_body.iter().for_each(|x| track_walk(x, state)); - state.end_scope(); - state.end_scope(); - } - ast::Stmt::IfCond(stmt) => { - match evaluate_type(&stmt.expr, state) { - Ok(_expr_type) => {} - Err(e) => { - state.errors_mut().extend(e); - } - } - - let true_bindings = predicate_implications(&stmt.expr, state, true); - let false_bindings = predicate_implications(&stmt.expr, state, false); - - // Record variables in each branch and their types (fuse them if they are the same) - state.start_branch(); - - // Use a narrowing scope for type guards in the true branch. - // This ensures narrowed types are visible in the branch body but don't - // participate in branch merging (they should revert after the branch). - state.start_narrowing_scope(); - true_bindings - .into_iter() - .for_each(|(k, v)| state.add_narrowing(k.as_str(), v)); - stmt.true_body.iter().for_each(|x| track_walk(x, state)); - state.end_narrowing_scope(); - - state.start_else_branch(); - - // Same for the false/else branch - state.start_narrowing_scope(); - false_bindings - .into_iter() - .for_each(|(k, v)| state.add_narrowing(k.as_str(), v)); - stmt.false_body.iter().for_each(|x| track_walk(x, state)); - state.end_narrowing_scope(); - - state.resolve_branch(); - } - ast::Stmt::WithBlock(_) => todo!(), - ast::Stmt::Set(stmt) => { - let expr_type = match evaluate_type(&stmt.expr, state) { - Ok(expr_type) => expr_type, - Err(e) => { - state.errors_mut().extend(e); - Type::Unknown - } - }; - - if let ast::Expr::Var(var) = &stmt.target { - state.add_variable(var.id, expr_type) - } - } - ast::Stmt::SetBlock(stmt) => { - let _target_type = evaluate_type(&stmt.target, state); - let _filter_type = stmt.filter.as_ref().map(|x| evaluate_type(x, state)); - stmt.body.iter().for_each(|x| track_walk(x, state)); - } - ast::Stmt::AutoEscape(_) => todo!(), - ast::Stmt::FilterBlock(_) => todo!(), - ast::Stmt::Macro(_stmt) => {} - ast::Stmt::CallBlock(_) => todo!(), - ast::Stmt::Do(_) => todo!(), - } -} - -pub(crate) fn get_variable_types(stmt: &Stmt, state: &mut PredefinedTypes) -> Vec { - track_walk(stmt, state); - state.errors().to_vec() -} - -/// For a given predicate, find all the implications on the contained types if -/// truthyness of the predicate is equal to the branch parameter. -/// -/// For example, in the context where `a: Number | null`, the expr `a` implies -/// `a: Number`. -/// So `predicate_implications(Var("a"), true)` should return `[("a", Number)]`. -/// `predicate_implications(Var("!a"), false)` should -/// return `[("a", Number)]`, because if `!a` is false, -/// then `a` is true. -/// -/// More complex examples (all assuming `branch: true`): -/// -/// Γ: { a: Number | null, b: Number | null } -/// (a && b) -> [(a: Number), (b: Number)] -/// -/// Γ: { a: Number | null } -/// (!!!!a) -> [(a: Number)] -/// -/// Γ: { a: Number | null } -/// (a && true) -> [(a: Number)] -/// -/// Γ: { a: Number | null } -/// (a && false) -> [] -/// -/// Γ: { a: Number | null } -/// (!!!a) -> [] -pub(super) fn predicate_implications<'a>( - expr: &'a ast::Expr<'a>, - context: &'a mut PredefinedTypes, - branch: bool, -) -> Vec<(String, Type)> { - use ast::Expr::*; - match expr { - Var(var_name) => context - .resolve(var_name.id) - .and_then(|var_type| truthy(&var_type)) - .map_or(vec![], |truthy_type| { - if branch { - vec![(var_name.id.to_string(), truthy_type)] - } else { - vec![(var_name.id.to_string(), Type::None)] - } - }), - UnaryOp(unary_op) => { - let next_branch = match unary_op.op { - UnaryOpKind::Not => !branch, - UnaryOpKind::Neg => branch, - }; - predicate_implications(&unary_op.expr, context, next_branch) - } - BinOp(binary_op) => match binary_op.op { - ast::BinOpKind::ScAnd => { - let mut left_implications = - predicate_implications(&binary_op.left, context, branch); - let right_implications = predicate_implications(&binary_op.right, context, branch); - left_implications.extend(right_implications); - left_implications - } - - ast::BinOpKind::ScOr => { - if branch { - // For `A or B` being TRUE: at least one is true - // We need to union the narrowed types for each variable - let left_implications = predicate_implications(&binary_op.left, context, true); - let right_implications = - predicate_implications(&binary_op.right, context, true); - - // Merge implications by variable name, creating unions where needed - let mut merged: std::collections::HashMap = - std::collections::HashMap::new(); - - for (var_name, var_type) in left_implications { - merged.insert(var_name, var_type); - } - - for (var_name, var_type) in right_implications { - merged - .entry(var_name) - .and_modify(|existing| { - *existing = Type::merge([existing.clone(), var_type.clone()]); - }) - .or_insert(var_type); - } - - merged.into_iter().collect() - } else { - // For `A or B` being FALSE: both must be false - // This is like AND on the false branches - let mut left_implications = - predicate_implications(&binary_op.left, context, false); - let right_implications = - predicate_implications(&binary_op.right, context, false); - left_implications.extend(right_implications); - left_implications - } - } - - ast::BinOpKind::Ne => { - let maybe_non_null_variable = match (&binary_op.left, &binary_op.right) { - (Var { .. }, Const(n)) if fuzzy_null(n) => Some(&binary_op.left), - (Const(n), Var { .. }) if fuzzy_null(n) => Some(&binary_op.right), - _ => None, - }; - if let Some(non_null_variable) = maybe_non_null_variable { - predicate_implications(non_null_variable, context, branch) - } else { - vec![] - } - } - // Narrow union attr access in the form of `if var.kind == "literal"` - ast::BinOpKind::Eq => { - // If statement is false then we don't care. - if !branch { - return vec![]; - } - - // Must be exactly `var_name == "const literal"` - let (get_attr, const_expr) = match (&binary_op.left, &binary_op.right) { - (GetAttr(get_attr), Const(const_expr)) - | (Const(const_expr), GetAttr(get_attr)) => (get_attr, const_expr), - - _ => return vec![], - }; - - // Nothing to narrow if it's not a var. - let Var(var) = &get_attr.expr else { - return vec![]; - }; - - narrow_attr_access_on_union_var(var, get_attr, const_expr, context) - } - _ => vec![], - }, - _ => vec![], - } -} - -/// Narrows the type of a variable based on the value of a const expression. -/// -/// Used for these cases: -/// -/// ```ignore -/// class UserMessage { -/// kind "user_message" -/// user_message String -/// } -/// -/// class AssistantMessage { -/// kind "assistant_message" -/// assistant_message String -/// } -/// -/// type Message = UserMessage | AssistantMessage -/// -/// {% if message.kind == "user_message" %} -/// {{ message.user_message }} -/// {% elif message.kind == "assistant_message" %} -/// {{ message.assistant_message }} -/// {% endif %} -/// ``` -/// -/// TODO: This function is very similar to `typecheck_attr_access_on_union` in -/// `expr.rs`. Reusing the code is not straightforward though (at least if we -/// want it to be readable), but we should try something because this is kind of -/// error prone if we add more types that need to be covered. -fn narrow_attr_access_on_union_var( - var: &Spanned>, - get_attr: &Spanned>, - const_expr: &Spanned, - context: &mut PredefinedTypes, -) -> Vec<(String, Type)> { - let Some(var_type) = context.resolve(var.id) else { - return vec![]; - }; - - let union_items = match &var_type { - Type::Union(items) => items, - Type::Alias { resolved, .. } => match resolved.as_ref() { - Type::Union(items) => items, - _ => return vec![], - }, - _ => return vec![], - }; - - let mut implications = vec![]; - let mut attr_type = None; - - let mut stack = Vec::from_iter(union_items.iter()); - - while let Some(union_item_type) = stack.pop() { - match union_item_type { - Type::ClassRef(class_name) => { - let (prop_type, err) = context.check_class_property( - &crate::evaluate_type::pretty_print::pretty_print(&get_attr.expr), - class_name, - get_attr.name, - get_attr.span(), - ); - - if err.is_some() { - return vec![]; - } - - match &attr_type { - None => attr_type = Some(prop_type.clone()), - - Some(known_type) => { - if !prop_type.equals_ignoring_literal_values(known_type) { - return vec![]; - } - } - } - - match prop_type { - Type::Literal(LiteralValue::String(literal_string)) => { - if let Some(value) = const_expr.value.as_str() { - if value == literal_string { - implications.push((var.id.to_string(), union_item_type.clone())); - } - } - } - Type::Literal(LiteralValue::Int(literal_int)) => { - if let Some(value) = const_expr.value.as_i64() { - if value == literal_int { - implications.push((var.id.to_string(), union_item_type.clone())); - } - } - } - // TODO: Jinja works with truthy | falsy, we can't check if - // this is literal true or false? - Type::Literal(LiteralValue::Bool(_)) => { - return vec![]; - } - - // TODO: Can't narrow against other types, there are no - // literal values to check. Maybe we could for enums? - _ => return vec![], - } - } - - // Recurse. - Type::Union(nested) => stack.extend(nested.iter()), - - // Resolve aliases. - Type::Alias { resolved, .. } => stack.push(resolved), - - _ => { - return vec![]; - } - } - } - - // Finding exactly one match means it's safe to infer the type. More than - // one is ambiguous. - if implications.len() == 1 { - implications - } else { - vec![] - } -} - -/// Whether an identifier is `None` or `none`. -fn fuzzy_null(t: &Spanned) -> bool { - t.value.to_string().as_str().to_lowercase() == "none" -} - -/// Type-narrowing by truthiness. The truthy version of a value's -/// type is a new type that would be implied by the value being truthy. -/// For example, `truthy( Number | null )` is `Number`, because if some -/// value `a: Number | null` is truthy, we can conclude that `a: Number`. -/// -/// Some types like `Number` offer no additional information if they -/// are truthy - in these cases we return None. -pub(super) fn truthy(ty: &Type) -> Option { - match ty { - Type::Unknown => None, - Type::Undefined => None, - Type::None => None, - Type::Int => None, - Type::Bigint => None, - Type::Float => None, - Type::Number => None, - Type::String => None, - Type::Bool => None, - Type::Literal(_) => None, - Type::List(_) => None, - Type::Map(_, _) => None, - Type::Tuple(_) => None, - Type::Union(variants) => { - let truthy_variants: Vec = variants - .iter() - .filter(|variant| !NULLISH.contains(variant)) - .cloned() - .collect(); - match truthy_variants.len() { - 0 => None, - 1 => Some(truthy_variants[0].clone()), - _ => Some(Type::Union(truthy_variants)), - } - } - Type::Both(x, y) => match (truthy(x), truthy(y)) { - (None, None) => None, - (Some(truthy_x), None) => Some(truthy_x), - (None, Some(truthy_y)) => Some(truthy_y), - (Some(truthy_x), Some(truthy_y)) => { - Some(Type::Both(Box::new(truthy_x), Box::new(truthy_y))) - } - }, - Type::ClassRef(_) => None, - Type::EnumTypeRef(_) => None, - Type::EnumValueRef(_) => None, - Type::FunctionRef(_) => None, - Type::Alias { resolved, .. } => truthy(resolved), - Type::RecursiveTypeAlias(_) => None, - Type::Image => None, - Type::Audio => None, - } -} - -const NULLISH: [Type; 2] = [Type::Undefined, Type::None]; - -#[cfg(test)] -mod tests { - use ast::{Expr, Spanned, Var}; - use minijinja::machinery::Span; - - use super::*; - use crate::JinjaContext; - - #[test] - fn truthy_union() { - let input = Type::Union(vec![Type::ClassRef("Foo".to_string()), Type::Undefined]); - let expected = Type::ClassRef("Foo".to_string()); - assert_eq!(truthy(&input).unwrap(), expected); - } - - #[test] - fn truthy_union_2() { - let input = Type::Union(vec![Type::Undefined, Type::ClassRef("Foo".to_string())]); - let expected = Type::ClassRef("Foo".to_string()); - assert_eq!(truthy(&input).unwrap(), expected); - } - - #[test] - fn implication_from_nullable() { - let mut context = PredefinedTypes::default(JinjaContext::Prompt); - context.add_variable("a", Type::Union(vec![Type::Int, Type::None])); - let expr = Expr::Var(Spanned::new(Var { id: "a" }, Span::default())); - let new_vars = predicate_implications(&expr, &mut context, true); - match new_vars.as_slice() { - [(name, Type::Int)] => { - assert_eq!(name.as_str(), "a"); - } - _ => panic!("Expected singleton list with Type::Int"), - } - } -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/test_expr.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/test_expr.rs deleted file mode 100644 index 25acfba621b..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/test_expr.rs +++ /dev/null @@ -1,550 +0,0 @@ -use baml_base::Literal as LiteralValue; -use minijinja::machinery::parse_expr; - -use crate::evaluate_type::{ - JinjaContext, - expr::evaluate_type, - types::{PredefinedTypes, Type}, -}; - -macro_rules! assert_evaluates_to { - ($expr:expr, $types:expr) => {{ - let parsed = parse_expr($expr); - assert!(parsed.is_ok(), "Failed to parse expression: {:?}", parsed); - let parsed = parsed.unwrap(); - - let result = evaluate_type(&parsed, &$types); - assert!( - result.is_ok(), - "Failed to evaluate expression: {:?}", - result - ); - result.unwrap() - }}; -} - -macro_rules! assert_fails_to { - ($expr:expr, $types:expr) => {{ - let parsed = parse_expr($expr); - assert!(parsed.is_ok(), "Failed to parse expression: {:?}", parsed); - let parsed = parsed.unwrap(); - - let result = evaluate_type(&parsed, &$types); - assert!( - result.is_err(), - "Expected evaluation to fail, but got: {:?}", - result - ); - result - .err() - .unwrap() - .iter() - .map(|x| x.message.clone()) - .collect::>() - }}; -} - -#[test] -fn test_evaluate_number() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!(assert_evaluates_to!("1.1 + 1", &types), Type::Number); -} - -#[test] -fn test_evaluate_bool() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!(assert_evaluates_to!("not 1.1", &types), Type::Bool); -} - -#[test] -fn test_evaluate_baml_null_alias() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!(assert_evaluates_to!("null", &types), Type::None); -} - -#[test] -fn test_evaluate_string() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!( - assert_fails_to!("ok ~ 1.1", &types), - vec!["Variable `ok` does not exist. Did you mean one of these: `_`, `ctx`?"] - ); - types.add_variable("ok", Type::String); - assert_eq!(assert_evaluates_to!("ok ~ 1.1", &types), Type::String); -} - -#[test] -fn test_evaluate_setting() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!( - assert_fails_to!("bar.f.g", &types), - vec!["Variable `bar` does not exist. Did you mean one of these: `_`, `ctx`?"] - ); - - types.add_class( - "Foo", - vec![("food".into(), Type::Float)].into_iter().collect(), - ); - types.add_variable("bar", Type::ClassRef("Foo".into())); - assert_eq!( - assert_fails_to!("bar.f", &types), - vec!["class Foo (bar) does not have a property 'f'"] - ); - - types.add_class("Foo", vec![("f".into(), Type::Int)].into_iter().collect()); - assert_eq!(assert_evaluates_to!("bar.f", &types), Type::Int); -} - -#[test] -fn test_ifexpr() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!( - assert_evaluates_to!("1 if true else 2", &types), - Type::Union(vec![ - Type::Literal(LiteralValue::Int(1)), - Type::Literal(LiteralValue::Int(2)) - ]) - ); - - assert_eq!( - assert_evaluates_to!("1 if true else '2'", &types), - Type::Union(vec![ - Type::Literal(LiteralValue::Int(1)), - Type::Literal(LiteralValue::String("2".to_string())) - ]) - ); - - assert_eq!( - assert_evaluates_to!("'1' if true else 2", &types), - Type::Union(vec![ - Type::Literal(LiteralValue::Int(2)), - Type::Literal(LiteralValue::String("1".to_string())) - ]) - ); - - types.add_function("AnotherFunc", Type::Float, vec![("arg".into(), Type::Bool)]); - - types.add_variable("BasicTest", Type::Int); - assert_eq!( - assert_evaluates_to!("BasicTest if true else AnotherFunc", &types), - Type::Union(vec![Type::Int, Type::FunctionRef("AnotherFunc".into())]) - ); -} - -#[test] -fn test_enum() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_enum("MyEnum", vec!["VALUE_A".into(), "VALUE_B".into()]); - types.add_variable("enum_arg", Type::EnumValueRef("VALUE_A".into())); - - assert_eq!( - assert_evaluates_to!("enum_arg", &types), - Type::EnumValueRef("VALUE_A".into()) - ); - assert_eq!( - assert_evaluates_to!("MyEnum.VALUE_A", &types), - Type::EnumValueRef("VALUE_A".into()) - ); - assert_eq!( - assert_evaluates_to!("MyEnum.VALUE_B", &types), - Type::EnumValueRef("VALUE_B".into()) - ); - assert_eq!( - assert_fails_to!("enum_arg == \"VALUE_A\"", &types), - vec![ - "Comparing enum VALUE_A to string variable - enum-string comparisons will soon be deprecated. Please see https://github.com/BoundaryML/baml/issues/2339." - ] - ); -} - -#[test] -fn test_enum_value_property_access() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_enum("MyEnum", vec!["VALUE_A".into(), "VALUE_B".into()]); - types.add_variable("enum_arg", Type::EnumValueRef("VALUE_A".into())); - - // Test accessing .value property on enum value - should return String - assert_eq!(assert_evaluates_to!("enum_arg.value", &types), Type::String); - - // Test accessing .value on enum accessed from enum type - assert_eq!( - assert_evaluates_to!("MyEnum.VALUE_A.value", &types), - Type::String - ); - - // Test accessing invalid property on enum value - should fail - assert_eq!( - assert_fails_to!("enum_arg.invalid_property", &types), - vec!["enum value VALUE_A (enum_arg) does not have a property 'invalid_property'"] - ); - - // Test accessing property on non-enum type - should fail with existing error - types.add_variable("string_var", Type::String); - assert_eq!( - assert_fails_to!("string_var.value", &types), - vec!["'string_var' is a string, expected class"] - ); -} - -#[test] -fn test_eval() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!(assert_evaluates_to!("1 + 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 - 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 * 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 / 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 ** 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 // 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 % 1", &types), Type::Number); - assert_eq!(assert_evaluates_to!("1 == 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 != 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 < 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 > 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 in 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 <= 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 >= 1", &types), Type::Bool); - assert_eq!(assert_evaluates_to!("1 ~ 1", &types), Type::String); -} - -#[test] -fn test_call_function() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("SomeFunc", Type::Float, vec![("arg".into(), Type::Bool)]); - - assert_eq!(assert_evaluates_to!("SomeFunc(true)", &types), Type::Float); - assert_eq!( - assert_fails_to!("SomeFunc(arg=1)", &types), - vec!["Function 'SomeFunc' expects argument 'arg' to be of type bool, but got literal[1]"] - ); - - types.add_function( - "AnotherFunc", - Type::Float, - vec![("arg".into(), Type::Bool), ("arg2".into(), Type::String)], - ); - assert_eq!( - assert_fails_to!("AnotherFunc(true)", &types), - vec!["Function 'AnotherFunc' expects 2 arguments, but got 1"] - ); - - assert_eq!( - assert_fails_to!("AnotherFunc(arg='true', arg2='1')", &types), - vec![ - r#"Function 'AnotherFunc' expects argument 'arg' to be of type bool, but got literal["true"]"# - ] - ); - - assert_eq!( - assert_fails_to!("AnotherFunc(arg=SomeFunc(true) ~ 1, arg2=1)", &types), - vec![ - "Function 'AnotherFunc' expects argument 'arg' to be of type bool, but got string", - "Function 'AnotherFunc' expects argument 'arg2' to be of type string, but got literal[1]" - ] - ); - - assert_eq!( - assert_evaluates_to!("AnotherFunc(true, arg2='1')", &types), - Type::Float - ); - - types.add_function( - "AnotherFunc", - Type::Float, - vec![ - ("arg".into(), Type::Bool), - ("arg2".into(), Type::String), - ("arg3".into(), Type::Number), - ], - ); - - assert_eq!( - assert_fails_to!("AnotherFunc(true, arg2='1')", &types), - vec!["Function 'AnotherFunc' expects 3 arguments, but got 2"] - ); - - assert_eq!( - assert_fails_to!("AnotherFunc(true, arg2='1')", &types), - vec!["Function 'AnotherFunc' expects 3 arguments, but got 2",] - ); - - assert_eq!( - assert_fails_to!("AnotherFunc(true, arg2='1', arg4=1)", &types), - vec![ - "Function 'AnotherFunc' expects argument 'arg3'", - "Function 'AnotherFunc' does not have an argument 'arg4'. Did you mean 'arg3'?" - ] - ); - - types.add_function( - "TakesLiteralFoo", - Type::Float, - vec![( - "arg".to_string(), - Type::Union(vec![ - Type::Literal(LiteralValue::String("Foo".to_string())), - Type::Literal(LiteralValue::String("Bar".to_string())), - ]), - )], - ); - - assert_eq!( - assert_evaluates_to!("TakesLiteralFoo('Foo')", &types), - Type::Float - ); -} - -#[test] -fn test_output_format() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!( - assert_evaluates_to!("ctx.output_format(prefix='hi')", &types), - Type::String - ); - - assert_eq!( - assert_evaluates_to!("ctx.output_format(prefix='1', or_splitter='1')", &types), - Type::String - ); - - assert_eq!( - assert_evaluates_to!( - "ctx.output_format(prefix='1', enum_value_prefix=none)", - &types - ), - Type::String - ); - - assert_eq!( - assert_evaluates_to!("ctx.output_format(render_null_as='omit')", &types), - Type::String - ); - - assert_eq!( - assert_fails_to!( - "ctx.output_format(prefix='1', always_hoist_enums=1)", - &types - ), - vec![ - "Function 'baml::OutputFormat' expects argument 'always_hoist_enums' to be of type (none | bool), but got literal[1]" - ] - ); - - assert_eq!( - assert_fails_to!( - "ctx.output_format(prefix='1', hoisted_class_prefix=1)", - &types - ), - vec![ - "Function 'baml::OutputFormat' expects argument 'hoisted_class_prefix' to be of type (none | string), but got literal[1]" - ] - ); - - assert_eq!( - assert_fails_to!("ctx.output_format(hoist_classes=1)", &types), - vec![ - "Function 'baml::OutputFormat' expects argument 'hoist_classes' to be of type (none | bool | literal[\"auto\"] | list[string]), but got literal[1]" - ] - ); - - assert_eq!( - assert_fails_to!("ctx.output_format(render_null_as=1)", &types), - vec![ - "Function 'baml::OutputFormat' expects argument 'render_null_as' to be of type (none | string), but got literal[1]" - ] - ); - - // TODO: There's a bug here, suggestions are not always the same, maybe some - // ordering issue or algorithm used to determine closest strings is not - // consistent. Code is in baml-lib/jinja/src/evaluate_type/mod.rs, - // TypeError::new_unknown_arg - // - // assert_eq!( - // assert_fails_to!("ctx.output_format(prefix='1', unknown=1)", &types), - // vec!["Function 'baml::OutputFormat' does not have an argument 'unknown'. Did you mean one of these: 'enum_value_prefix', 'hoist_classes', 'or_splitter'?"] - // ); -} - -#[test] -fn sum_filter() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - assert_eq!(assert_evaluates_to!(r#"[1,2,3]|sum"#, types), Type::Int); - assert_eq!( - assert_evaluates_to!(r#"[1.1,2.1,3.2]|sum"#, types), - Type::Float - ); - // // This would be nice, but it doesn't work. - // // Type checker says this is a subtype of `int[]`. - // // BUG. - // assert_eq!( - // assert_evaluates_to!(r#"[1.1,2,3]|sum"#, types), - // Type::Float - // ); - assert_eq!( - assert_fails_to!(r#"["hi", 1]|sum"#, types), - vec![r#"'[hi,1]' is a list[(literal[1] | literal["hi"])], expected (int|float)[]"#] - ); -} - -#[test] -fn test_function_reference_without_call_expr() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("SomeFunc", Type::Float, vec![]); - - let parsed = parse_expr("SomeFunc").unwrap(); - let result = evaluate_type(&parsed, &types); - assert!(result.is_err()); - let msgs: Vec<_> = result - .err() - .unwrap() - .into_iter() - .map(|e| e.message) - .collect(); - assert_eq!( - msgs, - vec![ - "Function 'SomeFunc' referenced without parentheses. Did you mean 'SomeFunc()'?" - .to_string() - ] - ); -} - -#[test] -fn test_filter_chain_type_propagation() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_class( - "Location", - vec![("country".into(), Type::String)].into_iter().collect(), - ); - types.add_variable("this", Type::ClassRef("Location".into())); - - // format() returns String, so chaining lower (expects String) should work - assert_eq!( - assert_evaluates_to!(r#"this|format(type="json")|lower"#, &types), - Type::String - ); - - // format() returns String, so chaining lower then regex_match should work - assert_eq!( - assert_evaluates_to!( - r#"this|format(type="json")|lower|regex_match("usa")"#, - &types - ), - Type::Bool - ); - - // String literal should work with lower filter - assert_eq!( - assert_evaluates_to!(r#""hello"|lower"#, &types), - Type::String - ); - - // String variable should work with lower filter - types.add_variable("string_var", Type::String); - assert_eq!( - assert_evaluates_to!(r#"string_var|lower"#, &types), - Type::String - ); - - // Chaining string filters: capitalize -> lower -> upper - assert_eq!( - assert_evaluates_to!(r#""hello"|capitalize|lower|upper"#, &types), - Type::String - ); - - // Int should NOT work with lower filter (should produce error) - types.add_variable("int_var", Type::Int); - assert_eq!( - assert_fails_to!(r#"int_var|lower"#, &types), - vec!["'int_var' is a int, expected string"] - ); -} - -#[test] -fn test_string_method_format() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("n", Type::Int); - - // String literal .format() should return String - assert_eq!( - assert_evaluates_to!(r#""{:,}".format(n)"#, &types), - Type::String - ); -} - -#[test] -fn test_string_method_on_variable() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("s", Type::String); - - // String variable .upper() should return String - assert_eq!(assert_evaluates_to!(r#"s.upper()"#, &types), Type::String); - - // String variable .lower() should return String - assert_eq!(assert_evaluates_to!(r#"s.lower()"#, &types), Type::String); -} - -#[test] -fn test_string_method_returns_bool() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("s", Type::String); - - assert_eq!( - assert_evaluates_to!(r#"s.startswith("hello")"#, &types), - Type::Bool - ); - - assert_eq!( - assert_evaluates_to!(r#"s.endswith("world")"#, &types), - Type::Bool - ); - - assert_eq!(assert_evaluates_to!(r#"s.isdigit()"#, &types), Type::Bool); -} - -#[test] -fn test_string_method_returns_int() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("s", Type::String); - - assert_eq!(assert_evaluates_to!(r#"s.find("x")"#, &types), Type::Int); - - assert_eq!(assert_evaluates_to!(r#"s.count("x")"#, &types), Type::Int); -} - -#[test] -fn test_string_method_returns_list() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("s", Type::String); - - assert_eq!( - assert_evaluates_to!(r#"s.split(",")"#, &types), - Type::List(Box::new(Type::String)) - ); - - assert_eq!( - assert_evaluates_to!(r#"s.splitlines()"#, &types), - Type::List(Box::new(Type::String)) - ); -} - -#[test] -fn test_string_method_invalid() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("s", Type::String); - - // Unknown method on string should still error - let errors = assert_fails_to!(r#"s.nonexistent()"#, &types); - assert_eq!(errors.len(), 2); - assert!(errors[0].contains("expected class")); -} - -#[test] -fn test_string_literal_method_chaining() { - let types = PredefinedTypes::default(JinjaContext::Prompt); - - // Method on literal string should return String - assert_eq!( - assert_evaluates_to!(r#""hello".upper()"#, &types), - Type::String - ); -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/test_stmt.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/test_stmt.rs deleted file mode 100644 index 914599118b9..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/test_stmt.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::evaluate_type::{ - JinjaContext, - stmt::get_variable_types, - types::{PredefinedTypes, Type}, -}; - -macro_rules! assert_evaluates_to { - ($expr:expr, $types:expr) => {{ - let parsed = minijinja::machinery::parse( - $expr, - "prompt", - minijinja::syntax::SyntaxConfig::default(), - // TODO: this is not entirely great, but good enough for this use case. - Default::default(), - ); - assert!(parsed.is_ok(), "Failed to parse template: {:?}", parsed); - let parsed = parsed.unwrap(); - - let result = get_variable_types(&parsed, &mut $types); - assert!( - result.is_empty(), - "Failed to evaluate expression: {:?}", - result - ); - }}; -} - -macro_rules! assert_fails_to { - ($expr:expr, $types:expr, $expected:expr) => {{ - let parsed = minijinja::machinery::parse( - $expr, - "prompt", - minijinja::syntax::SyntaxConfig::default(), - // TODO: this is not entirely great, but good enough for this use case. - Default::default(), - ); - assert!(parsed.is_ok(), "Failed to parse template: {:?}", parsed); - let parsed = parsed.unwrap(); - - let result = get_variable_types(&parsed, &mut $types); - assert!( - !result.is_empty(), - "Expected evaluation to fail, but got: {:?}", - result - ); - assert_eq!( - result.iter().map(|x| x.message.clone()).collect::>(), - $expected - ); - }}; -} - -#[test] -fn evaluate_undefined() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_fails_to!( - r#" - {{ prompt }} - "#, - types, - vec!["Variable `prompt` does not exist. Did you mean one of these: `_`, `ctx`?"] - ); -} - -#[test] -fn evaluate_number() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_evaluates_to!( - r#" - {%- set prompt = 1.1 + 1 -%} - {{ prompt }} - "#, - types - ); -} - -#[test] -fn evaluate_bool() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_evaluates_to!( - r#" - {%- set prompt = false -%} - {{ prompt }} - "#, - types - ); -} - -#[test] -fn evaluate_string() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - assert_evaluates_to!( - r#" - {%- set prompt = "hello" -%} - {{ prompt }} - "#, - types - ); -} - -#[test] -fn evaluate_pre_vars() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("prompt", Type::Bool); - assert_evaluates_to!( - r#" - {{ prompt }} - "#, - types - ); -} - -#[test] -fn function_call() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("prompt", Type::Bool); - assert_fails_to!( - r#" - {{ prompt() }} - "#, - types, - vec!["'prompt' is a bool, expected function"] - ); -} - -#[test] -fn function_call_1() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("prompt", Type::Bool, vec![]); - assert_evaluates_to!( - r#" - {{ prompt() }} - "#, - types - ); -} - -#[test] -fn function_call_2() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("prompt", Type::Bool, vec![("arg".into(), Type::String)]); - assert_fails_to!( - r#" - {% for x in items %} - {{ prompt(x) }} - {% endfor %} - "#, - types, - vec!["Variable `items` does not exist. Did you mean one of these: `_`, `ctx`?"] - ); -} - -#[test] -fn function_call_3() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("prompt", Type::Bool, vec![("arg".into(), Type::String)]); - types.add_variable("items", Type::List(Box::new(Type::String))); - assert_evaluates_to!( - r#" - {% for x in items %} - {{ prompt(x) }} - {% endfor %} - "#, - types - ); - - assert_fails_to!( - r#" - {% for x in items %} - {{ prompt(x) }} - {% endfor %} - {{ x }} - "#, - types, - vec!["Variable `x` does not exist. Did you mean one of these: `_`, `ctx`, `items`?"] - ); -} - -#[test] -fn function_call_4() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("prompt", Type::Bool, vec![("arg".into(), Type::String)]); - types.add_variable( - "dict_item", - Type::Map(Box::new(Type::Number), Box::new(Type::String)), - ); - assert_evaluates_to!( - r#" -{% for key, value in dict_item|items %} - Key: {{key}} - {{ prompt(value) }} -{% else %} - No items -{% endfor %} - "# - .trim(), - types - ); -} - -#[test] -fn loop_builtin() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("items", Type::List(Box::new(Type::String))); - assert_fails_to!( - r#" -{% for x in items %} - {{ loop.a.b }} - {{ x }} -{% endfor %} - "# - .trim(), - types, - vec!["class jinja::loop (loop) does not have a property 'a'",] - ); - - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("items", Type::List(Box::new(Type::String))); - assert_evaluates_to!( - r#" -{% for x in items %} - {{ loop.first }} - {{ x }} -{% endfor %} - "# - .trim(), - types - ); -} - -#[test] -fn if_else() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("prompt", Type::String); - types.add_function("Foo", Type::Bool, vec![("arg".into(), Type::String)]); - assert_fails_to!( - r#" -{% if prompt == 'a' -%} - {% set x = 1 %} -{%- elif prompt == 'abc' -%} - {% set x = '2' %} -{%- else -%} - {% set y = '[1]' %} -{%- endif %} - {{ Foo(x) }} - "# - .trim(), - types, - vec![ - r#"Function 'Foo' expects argument 'arg' to be of type string, but got (undefined | literal[1] | literal["2"])"# - ] - ); - - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_variable("prompt", Type::String); - types.add_function("Foo", Type::Bool, vec![("arg".into(), Type::String)]); - assert_evaluates_to!( - r#" -{% if prompt == 'a' -%} - {% set x = '1' %} -{%- elif prompt == 'abc' -%} - {% set x = '2' %} -{%- else -%} - {% set x = '[1]' %} -{%- endif %} - {{ Foo(x) }} - "# - .trim(), - types - ); -} - -#[test] -fn function_reference_without_call_in_template() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_function("MyTemplateString", Type::String, vec![]); - assert_fails_to!( - r#" -{{ MyTemplateString }} -"# - .trim(), - types, - vec!["Function 'MyTemplateString' referenced without parentheses. Did you mean 'MyTemplateString()'?".to_string()] - ); -} diff --git a/baml_language/crates/sys_jinja_types/src/evaluate_type/types.rs b/baml_language/crates/sys_jinja_types/src/evaluate_type/types.rs deleted file mode 100644 index b9eef9a169f..00000000000 --- a/baml_language/crates/sys_jinja_types/src/evaluate_type/types.rs +++ /dev/null @@ -1,813 +0,0 @@ -use core::panic; -use std::{ - collections::{HashMap, HashSet}, - ops::BitOr, - vec, -}; - -use baml_base::Literal as LiteralValue; -use indexmap::IndexMap; -use minijinja::machinery::{ - Span, - ast::{Call, Spanned}, -}; - -use super::TypeError; - -#[derive(Debug, Clone, PartialEq)] -pub struct EnumDefinition { - pub name: String, - pub values: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct EnumValueDefinition { - pub name: String, - pub alias: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum Type { - Unknown, - Undefined, - None, - Int, - Bigint, - Float, - // Too large to handle - Number, - String, - Bool, - Literal(LiteralValue), - List(Box), - Map(Box, Box), - Tuple(Vec), - Union(Vec), - // It is simultaneously two types, whichever fits best - Both(Box, Box), - ClassRef(String), - EnumTypeRef(String), - EnumValueRef(String), - FunctionRef(String), - /// TODO: This should be `AliasRef(String)` but functions like - /// [`Self::is_subtype_of`] or [`Self::bitor`] don't have access to the - /// [`PredefinedTypes`] instance, so we can't grab type resolutions from - /// there. - /// - /// We'll just store all the necessary information in the type itself for - /// now. - Alias { - name: String, - target: Box, - resolved: Box, - }, - /// TODO: This one could store the target so that we can report what it - /// points to instead of just the name. - RecursiveTypeAlias(String), - Image, - Audio, -} - -impl Type { - /// Subtyping for the Jinja template type lattice. - /// - /// This is a subtyping implementation for a **separate dialect**, distinct - /// from BAML's single canonical relation `baml_type::normalize::is_subtype` - /// (behind the `TypeContext` trait, shared by the compiler and the runtime). - /// It is intentionally isolated: Jinja templates have their own value - /// semantics (`Bigint` never widens to anything — see the `Bigint` arm - /// below), so this impl mirrors neither the scalar nor the container rules of - /// the BAML algebra. - /// - /// Do **not** keep this in sync with the TIR rules by reflex — it is - /// a separate dialect by design. Any change here should be motivated - /// by Jinja template semantics, not by the BAML type system. - pub fn is_subtype_of(&self, other: &Self) -> bool { - if self == other { - return true; - } - if let Type::Union(items) = other { - if items.iter().any(|x| self.is_subtype_of(x)) { - return true; - } - } - match (self, other) { - (Type::Unknown, _) => true, - (_, Type::Unknown) => true, - (_, Type::Undefined) => false, - (_, Type::None) => false, - (Type::Undefined, _) => false, - (Type::None, _) => false, - - // Handle types that nest other types. - (Type::List(l0), Type::List(r0)) => l0.is_subtype_of(r0), - (Type::List(_), _) => false, - (Type::Map(l0, l1), Type::Map(r0, r1)) => l0.is_subtype_of(r0) && l1.is_subtype_of(r1), - (Type::Map(_, _), _) => false, - - (Type::Int, Type::Number) => true, - (Type::Int, _) => false, - - (Type::Bigint, _) => false, - - (Type::Float, Type::Number) => true, - (Type::Float, _) => false, - - // This is cause jinja is dumb and doesn't know the difference between int and float - (Type::Number, Type::Float | Type::Int) => true, - (Type::Number, _) => false, - - (Type::Literal(LiteralValue::Int(_)), Type::Int | Type::Number) => true, - (Type::Literal(LiteralValue::Bool(_)), Type::Bool) => true, - (Type::Literal(LiteralValue::String(_)), Type::String) => true, - (Type::Literal(_), _) => false, - - (Type::Union(l0), _) => l0.iter().all(|x| x.is_subtype_of(other)), - - (Type::Both(l0, r0), _) => l0.is_subtype_of(other) || r0.is_subtype_of(other), - (_, Type::Both(l0, r0)) => self.is_subtype_of(l0) && self.is_subtype_of(r0), - - (Type::Tuple(l0), Type::Tuple(r0)) => { - if l0.len() != r0.len() { - return false; - } - l0.iter().zip(r0.iter()).all(|(l, r)| l.is_subtype_of(r)) - } - (Type::Tuple(_), _) => false, - - (Type::ClassRef(_), _) => false, - (Type::EnumTypeRef(_), _) => false, - (Type::EnumValueRef(_), _) => false, - (Type::FunctionRef(_), _) => false, - (Type::Alias { resolved, .. }, _) => resolved.is_subtype_of(other), - (Type::RecursiveTypeAlias(_), _) => false, - (Type::Image, _) => false, - (Type::Audio, _) => false, - (Type::String, _) => false, - (Type::Bool, _) => false, - } - } - - /// Used for type narrowing. - /// - /// Basic example: - /// - /// ```ignore - /// class UserMessage { - /// kind "user_message" - /// user_message String - /// } - /// - /// class AssistantMessage { - /// kind "assistant_message" - /// assistant_message String - /// } - /// ``` - /// - /// Now on narrowing: - /// - /// ```ignore - /// {% if message.kind == "user_message" %} - /// ``` - pub fn equals_ignoring_literal_values(&self, other: &Self) -> bool { - match (self, other) { - (Type::Literal(left), Type::Literal(right)) => matches!( - (left, right), - (LiteralValue::Int(_), LiteralValue::Int(_)) - | (LiteralValue::Bool(_), LiteralValue::Bool(_)) - | (LiteralValue::String(_), LiteralValue::String(_)) - ), - - // Fallback to PartialEq - _ => self == other, - } - } - - // pub fn matches(&self, r: &Self) -> bool { - // match (self, r) { - // (Self::Unknown, Self::Unknown) => true, - // (Self::Unknown, _) => true, - // (_, Self::Unknown) => true, - // (Self::Number, Self::Int | Self::Float) => true, - // (Self::Int | Self::Float, Self::Number) => true, - // (Self::List(l0), Self::List(r0)) => l0.matches(r0), - // (Self::Map(l0, l1), Self::Map(r0, r1)) => l0.matches(r0) && l1.matches(r1), - // (Self::Union(l0), Self::Union(r0)) => { - // // Sort l0 and r0 to make sure the order doesn't matter - // let mut l0 = l0.clone(); - // let mut r0 = r0.clone(); - // l0.sort(); - // r0.sort(); - // l0 == r0 - // } - // (l0, Self::Union(r0)) => r0.iter().any(|x| l0.matches(x)), - // (Self::ClassRef(l0), Self::ClassRef(r0)) => l0 == r0, - // (Self::FunctionRef(l0), Self::FunctionRef(r0)) => l0 == r0, - // _ => core::mem::discriminant(self) == core::mem::discriminant(r), - // } - // } - - pub fn name(&self) -> String { - match self { - Type::Unknown => "".into(), - Type::Undefined => "undefined".into(), - Type::None => "none".into(), - Type::Int => "int".into(), - Type::Bigint => "bigint".into(), - Type::Float => "float".into(), - Type::Number => "number".into(), - Type::String => "string".into(), - Type::Bool => "bool".into(), - Type::Literal(value) => format!("literal[{value}]"), - Type::List(l) => format!("list[{}]", l.name()), - Type::Map(k, v) => format!("map[{}, {}]", k.name(), v.name()), - Type::Tuple(v) => format!( - "({})", - v.iter().map(|x| x.name()).collect::>().join(", ") - ), - Type::Union(v) => format!( - "({})", - v.iter().map(|x| x.name()).collect::>().join(" | ") - ), - Type::Both(l, r) => format!("{} & {}", l.name(), r.name()), - Type::ClassRef(name) => format!("class {name}"), - Type::EnumTypeRef(name) => format!("enum definition {name}"), - Type::EnumValueRef(name) => format!("enum {name}"), - Type::FunctionRef(name) => format!("function {name}"), - Type::Alias { name, resolved, .. } => { - format!("type alias {name} (resolves to {})", resolved.name()) - } - Type::RecursiveTypeAlias(name) => format!("recursive type alias {name}"), - Type::Image => "image".into(), - Type::Audio => "audio".into(), - } - } - - pub fn is_optional(&self) -> bool { - match self { - Type::None => true, - Type::Union(v) => v.iter().any(Type::is_optional), - _ => false, - } - } - - pub fn merge(v: I) -> Type - where - I: IntoIterator, - { - v.into_iter().fold(Type::Unknown, |acc, x| acc | x) - } -} - -impl BitOr for Type { - type Output = Type; - - fn bitor(self, rhs: Type) -> Type { - match (self, rhs) { - (Type::Unknown, t) => t, - (t, Type::Unknown) => t, - (Type::Union(mut v1), Type::Union(v2)) => { - v1.extend(v2); - // Remove duplicates - v1.sort(); - v1.dedup(); - Type::Union(v1) - } - (Type::Union(mut v), t) => { - v.push(t); - v.sort(); - v.dedup(); - Type::Union(v) - } - (t, Type::Union(mut v)) => { - v.push(t); - v.sort(); - v.dedup(); - Type::Union(v) - } - (t1, t2) => { - if t1.is_subtype_of(&t2) { - return t1; - } - if t2.is_subtype_of(&t1) { - return t2; - } - let mut types = vec![t1, t2]; - types.sort(); - Type::Union(types) - } - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum Scope { - CodeBlock(IndexMap), - Branch(IndexMap, IndexMap, bool), - /// A scope for type narrowing (e.g., from type guards in if conditions). - /// Variables in this scope shadow outer variables but don't participate - /// in branch merging. When the scope ends, the narrowed types disappear - /// and the original types are restored. - Narrowing(IndexMap), -} - -#[derive(Debug, Clone, PartialEq)] -pub struct PredefinedTypes { - functions: IndexMap)>, - classes: HashMap>, - enum_definitions: IndexMap, - /// TODO: See the comment for [`Type::AliasRef`]. - /// - /// We should use this but we can't without a significant refactor. - aliases: IndexMap, - // Variable name <--> Definition - variables: IndexMap, - scopes: Vec, - - errors: Vec, -} - -pub enum JinjaContext { - Prompt, - Parsing, -} - -impl PredefinedTypes { - pub fn variable_names(&self) -> Vec { - self.variables - .keys() - .chain(self.scopes.iter().flat_map(|s| match s { - Scope::CodeBlock(vars) => vars.keys(), - Scope::Branch(on_true, on_false, cond) => { - if *cond { - on_true.keys() - } else { - on_false.keys() - } - } - Scope::Narrowing(vars) => vars.keys(), - })) - .map(String::to_owned) - .collect() - } - - pub fn default(context: JinjaContext) -> Self { - Self { - functions: IndexMap::from([ - ( - "baml::Chat".into(), - ( - Type::String, - vec![ - ("role".into(), Type::String), - ( - "cache_control".into(), - Type::merge(vec![ - Type::Map(Box::new(Type::String), Box::new(Type::Unknown)), - Type::None, - ]), - ), - ], - ), - ), - ( - "baml::OutputFormat".into(), - ( - Type::String, - vec![ - ("prefix".into(), Type::merge(vec![Type::String, Type::None])), - ( - "or_splitter".into(), - Type::merge(vec![Type::String, Type::None]), - ), - ( - "enum_value_prefix".into(), - Type::merge(vec![Type::String, Type::None]), - ), - ( - "always_hoist_enums".into(), - Type::merge(vec![Type::Bool, Type::None]), - ), - ( - "hoisted_class_prefix".into(), - Type::merge(vec![Type::String, Type::None]), - ), - ( - "hoist_classes".into(), - Type::merge(vec![ - Type::None, - Type::Bool, - Type::Literal(LiteralValue::String(String::from("auto"))), - Type::List(Box::new(Type::String)), - ]), - ), - ( - "quote_class_fields".into(), - Type::merge(vec![Type::Bool, Type::None]), - ), - ( - "render_null_as".into(), - Type::merge(vec![Type::String, Type::None]), - ), - ], - ), - ), - ]), - classes: HashMap::from([ - ( - "baml::Client".into(), - IndexMap::from([ - ("name".into(), Type::String), - ("provider".into(), Type::String), - ]), - ), - ( - "baml::Context".into(), - IndexMap::from([ - ( - "output_format".into(), - Type::Both( - Type::String.into(), - Type::FunctionRef("baml::OutputFormat".into()).into(), - ), - ), - ("client".into(), Type::ClassRef("baml::Client".into())), - ( - "tags".into(), - Type::Map(Box::new(Type::String), Box::new(Type::String)), - ), - ]), - ), - ( - "baml::BuiltIn".into(), - IndexMap::from([ - ("chat".into(), Type::FunctionRef("baml::Chat".into())), - ("role".into(), Type::FunctionRef("baml::Chat".into())), - ]), - ), - ( - "jinja::loop".into(), - IndexMap::from([ - ("index".into(), Type::Int), - ("index0".into(), Type::Int), - ("revindex".into(), Type::Int), - ("revindex0".into(), Type::Int), - ("first".into(), Type::Bool), - ("last".into(), Type::Bool), - ("length".into(), Type::Int), - ("depth".into(), Type::Int), - ("depth0".into(), Type::Int), - ]), - ), - ]), - enum_definitions: Default::default(), - variables: match context { - JinjaContext::Prompt => IndexMap::from([ - ("ctx".into(), Type::ClassRef("baml::Context".into())), - ("_".into(), Type::ClassRef("baml::BuiltIn".into())), - ]), - JinjaContext::Parsing => Default::default(), - }, - aliases: IndexMap::new(), - scopes: Vec::new(), - errors: Vec::new(), - } - } - - pub fn start_scope(&mut self) { - self.scopes.push(Scope::CodeBlock(IndexMap::new())); - } - - pub fn end_scope(&mut self) { - self.scopes.pop(); - } - - /// Start a narrowing scope for type guards in if conditions. - /// Variables added via `add_narrowing` will shadow outer variables - /// but won't participate in branch merging. - pub fn start_narrowing_scope(&mut self) { - self.scopes.push(Scope::Narrowing(IndexMap::new())); - } - - /// End the current narrowing scope, restoring original variable types. - pub fn end_narrowing_scope(&mut self) { - match self.scopes.pop() { - Some(Scope::Narrowing(_)) => {} - _ => panic!("Cannot end narrowing scope without starting one"), - } - } - - /// Add a narrowed variable type to the current narrowing scope. - /// This should only be called after `start_narrowing_scope`. - pub fn add_narrowing(&mut self, name: &str, t: Type) { - match self.scopes.last_mut() { - Some(Scope::Narrowing(vars)) => { - vars.insert(name.to_string(), t); - } - _ => panic!("Cannot add narrowing without a Narrowing scope"), - } - } - - pub fn start_branch(&mut self) { - self.scopes - .push(Scope::Branch(IndexMap::new(), IndexMap::new(), true)); - } - - pub fn start_else_branch(&mut self) { - match self.scopes.last_mut() { - Some(Scope::Branch(_, _, x)) => { - *x = false; - } - _ => { - panic!("Cannot start else branch without starting a branch"); - } - } - } - - pub fn resolve_branch(&mut self) { - let (true_vars, false_vars) = match self.scopes.pop() { - Some(Scope::Branch(true_vars, false_vars, _)) => (true_vars, false_vars), - _ => { - panic!("Cannot resolve branch without starting a branch"); - } - }; - - // Any vars that are in both branches are merged - // Any vars that are only in one branch, unioned with undefined - - let mut new_vars = HashMap::new(); - for (name, t) in true_vars.iter() { - if let Some(false_t) = false_vars.get(name) { - new_vars.insert(name.clone(), t.clone() | false_t.clone()); - } else { - new_vars.insert(name.clone(), t.clone() | Type::Undefined); - } - } - for (name, t) in false_vars.iter() { - if !new_vars.contains_key(name) { - new_vars.insert(name.clone(), t.clone() | Type::Undefined); - } - } - - new_vars.iter().for_each(|(name, t)| { - self.add_variable(name, t.clone()); - }); - } - - pub fn errors_mut(&mut self) -> &mut Vec { - &mut self.errors - } - - pub fn errors(&self) -> &[TypeError] { - &self.errors - } - - pub fn resolve(&self, name: &str) -> Option { - if let Some(t) = self.as_variable(name) { - return Some(t.clone()); - } - if self.as_function(name).is_some() { - return Some(Type::FunctionRef(name.to_string())); - } - if self.as_class(name).is_some() { - return Some(Type::ClassRef(name.to_string())); - } - if self.as_enum(name).is_some() { - return Some(Type::EnumTypeRef(name.to_string())); - } - None - } - - pub fn as_variable(&self, name: &str) -> Option<&Type> { - self.scopes - .iter() - .rev() - .find_map(|scope| match scope { - Scope::CodeBlock(vars) => vars.get(name), - Scope::Branch(true_vars, false_vars, cond) => { - if *cond { - true_vars.get(name) - } else { - false_vars.get(name) - } - } - Scope::Narrowing(vars) => vars.get(name), - }) - .or_else(|| self.variables.get(name)) - } - - pub fn as_class(&self, name: &str) -> Option<&IndexMap> { - self.classes.get(name) - } - - pub fn as_enum(&self, name: &str) -> Option<&EnumDefinition> { - self.enum_definitions.get(name) - } - - pub fn as_function(&self, name: &str) -> Option<&(Type, Vec<(String, Type)>)> { - self.functions.get(name) - } - - pub fn add_function(&mut self, name: &str, ret: Type, args: Vec<(String, Type)>) { - self.functions.insert(name.to_string(), (ret, args)); - } - - pub fn add_class(&mut self, name: &str, fields: IndexMap) { - self.classes.insert(name.to_string(), fields); - } - - pub fn add_enum(&mut self, name: &str, values: Vec) { - self.add_enum_with_metadata( - name, - values - .into_iter() - .map(|v| EnumValueDefinition { - name: v, - alias: None, - }) - .collect(), - ); - } - - pub fn add_enum_with_metadata(&mut self, name: &str, values: Vec) { - self.enum_definitions.insert( - name.to_string(), - EnumDefinition { - name: name.to_string(), - values, - }, - ); - } - - pub fn add_alias(&mut self, name: &str, target: Type) { - self.aliases.insert(name.to_string(), target); - } - - pub fn add_variable(&mut self, name: &str, t: Type) { - // Find the topmost non-Narrowing scope to add the variable to. - // Narrowing scopes are skipped so that assignments (e.g., {% set x = 1 %}) - // go to the underlying Branch/CodeBlock scope for proper branch merging. - for scope in self.scopes.iter_mut().rev() { - match scope { - Scope::Narrowing(_) => continue, // Skip narrowing scopes - Scope::Branch(true_vars, false_vars, branch_cond) => { - if *branch_cond { - true_vars.insert(name.to_string(), t); - } else { - false_vars.insert(name.to_string(), t); - } - return; - } - Scope::CodeBlock(vars) => { - vars.insert(name.to_string(), t); - return; - } - } - } - // No scope found, add to root variables - self.variables.insert(name.to_string(), t); - } - - pub fn check_class_property( - &self, - variable_name: &str, - class: &str, - prop: &str, - span: Span, - ) -> (Type, Option) { - if let Some(fields) = self.as_class(class) { - if let Some(t) = fields.get(prop) { - return (t.clone(), None); - } else { - return ( - Type::Unknown, - Some(TypeError::new_property_not_defined( - variable_name, - class, - prop, - span, - )), - ); - } - } - (Type::Unknown, Some(TypeError::new_class_not_defined(class))) - } - - pub fn check_enum_property( - &self, - variable_name: &str, - enum_name: &str, - enum_value: &str, - span: Span, - ) -> (Type, Option) { - if let Some(enum_def) = self.as_enum(enum_name) { - if enum_def.values.iter().any(|v| v.name == enum_value) { - return (Type::EnumValueRef(enum_value.to_string()), None); - } else { - return ( - Type::Unknown, - Some(TypeError::new_property_not_defined( - variable_name, - enum_name, - enum_value, - span, - )), - ); - } - } - ( - Type::Unknown, - Some(TypeError::new_enum_not_defined(enum_name)), - ) - } - - pub fn check_function_args( - &self, - (func, expr): (&str, &Spanned), - positional_args: &[Type], - kwargs: &IndexMap<&str, Type>, - ) -> (Type, Vec) { - let span = expr.span(); - let val = self.as_function(func); - if val.is_none() { - return ( - Type::Unknown, - vec![TypeError::new_invalid_type( - &expr.expr, - &Type::Unknown, - func, - span, - )], - ); - } - let (ret, args) = val.unwrap(); - let mut errors = Vec::new(); - - // Check how many args are required. - let mut optional_args = vec![]; - for (name, t) in args.iter().rev() { - if !t.is_optional() { - break; - } - optional_args.push(name); - } - let required_args = args.len() - optional_args.len(); - - // Check count - if positional_args.len() + kwargs.len() < required_args - || (positional_args.len() + kwargs.len()) > args.len() - { - errors.push(TypeError::new_wrong_arg_count( - func, - span, - args.len(), - positional_args.len() + kwargs.len(), - )); - } else { - let mut unused_args = args.iter().map(|(name, _)| name).collect::>(); - // Check types - for (i, (name, t)) in args.iter().enumerate() { - if i < positional_args.len() { - unused_args.remove(name); - let arg_t = &positional_args[i]; - if !arg_t.is_subtype_of(t) { - errors.push(TypeError::new_wrong_arg_type( - func, - span, - name, - span, - t.clone(), - arg_t.clone(), - )); - } - } else if let Some(arg_t) = kwargs.get(name.as_str()) { - unused_args.remove(name); - if !arg_t.is_subtype_of(t) { - errors.push(TypeError::new_wrong_arg_type( - func, - span, - name, - span, - t.clone(), - arg_t.clone(), - )); - } - } else if !optional_args.contains(&name) { - errors.push(TypeError::new_missing_arg(func, span, name)); - } - } - - kwargs.iter().for_each(|(name, _)| { - if !args.iter().any(|(arg_name, _)| arg_name == name) { - errors.push(TypeError::new_unknown_arg( - func, - span, - name, - unused_args.clone(), - )); - } - }); - } - (ret.clone(), errors) - } -} diff --git a/baml_language/crates/sys_jinja_types/src/lib.rs b/baml_language/crates/sys_jinja_types/src/lib.rs deleted file mode 100644 index 3f0851b4844..00000000000 --- a/baml_language/crates/sys_jinja_types/src/lib.rs +++ /dev/null @@ -1,297 +0,0 @@ -#![allow( - clippy::default_trait_access, - clippy::doc_markdown, - clippy::enum_glob_use, - clippy::explicit_iter_loop, - clippy::from_iter_instead_of_collect, - clippy::manual_let_else, - clippy::manual_string_new, - clippy::needless_for_each, - clippy::needless_pass_by_value, - clippy::redundant_closure_for_method_calls, - clippy::redundant_else, - clippy::semicolon_if_nothing_returned, - clippy::stable_sort_primitive -)] - -mod evaluate_type; - -use evaluate_type::get_variable_types; -pub use evaluate_type::{ - EnumDefinition, EnumValueDefinition, JinjaContext, PredefinedTypes, Type, TypeError, -}; - -#[derive(Debug)] -pub struct ValidationError { - pub errors: Vec, - pub parsing_errors: Option, -} - -impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - for err in &self.errors { - writeln!(f, "{err}")?; - } - Ok(()) - } -} - -impl std::error::Error for ValidationError {} - -pub fn validate_template( - name: &str, - template: &str, - types: &mut PredefinedTypes, -) -> Result<(), ValidationError> { - let parsed = - match minijinja::machinery::parse(template, name, Default::default(), Default::default()) { - Ok(parsed) => parsed, - Err(err) => { - return Err(ValidationError { - errors: vec![], - parsing_errors: Some(err), - }); - } - }; - - let errs = get_variable_types(&parsed, types); - - if errs.is_empty() { - Ok(()) - } else { - Err(ValidationError { - errors: errs, - parsing_errors: None, - }) - } -} - -#[cfg(test)] -mod tests { - use baml_base::Literal as LiteralValue; - use indexmap::IndexMap; - - use super::*; - - fn mk_params() -> PredefinedTypes { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - types.add_class("Foo", IndexMap::from([("name".to_string(), Type::String)])); - types.add_variable( - "foo", - Type::Union(vec![Type::None, Type::ClassRef("Foo".to_string())]), - ); - types.add_variable( - "foo2", - Type::Union(vec![Type::None, Type::ClassRef("Foo".to_string())]), - ); - types - } - - #[test] - fn test_type_narrowing_not_narrowed() { - let mut types = mk_params(); - let err_unnarrowed = validate_template( - "test", - r#" - {{ foo.name }} - "#, - &mut types, - ) - .expect_err("Should fail") - .errors - .into_iter() - .next() - .unwrap(); - assert_eq!( - err_unnarrowed.message(), - "cannot access property 'name' on 'foo': union contains non-class type none" - ); - } - - #[test] - fn test_type_narrowing_truthy() { - let mut types = mk_params(); - validate_template( - "test", - r#" - {% if foo %} - {{ foo.name }} - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } - - #[test] - fn test_type_narrowing_truthy_and() { - let mut types = mk_params(); - validate_template( - "test", - r#" - {% if (foo and foo2) %} - {{ foo.name }} - {{ foo2.name }} - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } - - #[test] - fn test_type_narrowing_ne_none() { - let mut types = mk_params(); - validate_template( - "test", - r#" - {% if foo!=none %} - {{ foo.name }} - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } - - #[test] - fn test_should_narrow_union_type() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - - types.add_class( - "UserMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("user_message".to_string())), - "user_message".to_string() => Type::String, - } - ); - - types.add_class( - "AssistantMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("assistant_message".to_string())), - "assistant_message".to_string() => Type::String, - }, - ); - - types.add_variable( - "message", - Type::Union(vec![ - Type::ClassRef("UserMessage".to_string()), - Type::ClassRef("AssistantMessage".to_string()), - ]), - ); - - validate_template( - "test", - r#" - {% if message.kind == "user_message" %} - User Message - {% elif message.kind == "assistant_message" %} - Assistant Message - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } - - #[test] - fn test_should_infer_narrowed_type_on_if_branch() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - - types.add_class( - "UserMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("user_message".to_string())), - "user_message".to_string() => Type::String, - } - ); - - types.add_class( - "AssistantMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("assistant_message".to_string())), - "assistant_message".to_string() => Type::String, - }, - ); - - types.add_variable( - "message", - Type::Union(vec![ - Type::ClassRef("UserMessage".to_string()), - Type::ClassRef("AssistantMessage".to_string()), - ]), - ); - - validate_template( - "test", - r#" - {% if message.kind == "user_message" %} - {{ message.user_message }} - {% elif message.kind == "assistant_message" %} - {{ message.assistant_message }} - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } - - #[test] - fn test_should_infer_narrowed_type_on_if_branch_with_alias() { - let mut types = PredefinedTypes::default(JinjaContext::Prompt); - - types.add_class( - "UserMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("user_message".to_string())), - "user_message".to_string() => Type::String, - } - ); - - types.add_class( - "AssistantMessage", - indexmap::indexmap! { - "kind".to_string() => Type::Literal(LiteralValue::String("assistant_message".to_string())), - "assistant_message".to_string() => Type::String, - }, - ); - - types.add_alias( - "Message", - Type::Union(vec![ - Type::ClassRef("UserMessage".to_string()), - Type::ClassRef("AssistantMessage".to_string()), - ]), - ); - - types.add_variable( - "message", - Type::Alias { - name: "Message".to_string(), - target: Box::new(Type::Union(vec![ - Type::ClassRef("UserMessage".to_string()), - Type::ClassRef("AssistantMessage".to_string()), - ])), - resolved: Box::new(Type::Union(vec![ - Type::ClassRef("UserMessage".to_string()), - Type::ClassRef("AssistantMessage".to_string()), - ])), - }, - ); - - validate_template( - "test", - r#" - {% if message.kind == "user_message" %} - {{ message.user_message }} - {% elif message.kind == "assistant_message" %} - {{ message.assistant_message }} - {% endif %} - "#, - &mut types, - ) - .expect("Should succeed"); - } -} diff --git a/baml_language/crates/sys_llm/Cargo.toml b/baml_language/crates/sys_llm/Cargo.toml index e9c8046e133..8cacd6eb9c5 100644 --- a/baml_language/crates/sys_llm/Cargo.toml +++ b/baml_language/crates/sys_llm/Cargo.toml @@ -24,7 +24,6 @@ bex_heap = { workspace = true } bex_resource_types = { workspace = true } bex_sap = { workspace = true } bex_vm_types = { workspace = true } -sys_jinja = { workspace = true } sys_types = { workspace = true } async-trait = { workspace = true } aws-bedrock = { workspace = true } @@ -33,7 +32,6 @@ base64 = { workspace = true } google-cloud-auth = { workspace = true } indexmap = { workspace = true } infer = { workspace = true } -minijinja = { workspace = true } num-bigint = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/baml_language/crates/sys_llm/src/jinja/error.rs b/baml_language/crates/sys_llm/src/jinja/error.rs deleted file mode 100644 index b5ff5e8c3eb..00000000000 --- a/baml_language/crates/sys_llm/src/jinja/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Error types for the Jinja runtime. - -use thiserror::Error; - -/// Errors that can occur during prompt rendering. -#[derive(Debug, Error)] -pub enum RenderPromptError { - /// Template syntax or rendering error from minijinja. - #[error("Template error: {0}")] - TemplateError(#[from] minijinja::Error), - - /// Missing or invalid template variable. - #[error("Missing variable: {name}")] - MissingVariable { name: String }, - - /// Value could not be converted to a Jinja-compatible form (e.g. Handle or unsupported type). - #[error("Failed to convert value to Jinja value: {reason}")] - ConversionError { reason: String }, -} diff --git a/baml_language/crates/sys_llm/src/jinja/mod.rs b/baml_language/crates/sys_llm/src/jinja/mod.rs deleted file mode 100644 index a269714f5a9..00000000000 --- a/baml_language/crates/sys_llm/src/jinja/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Jinja template runtime for BAML prompts. -//! -//! This module provides: -//! - `render_prompt()` - Main entry point for rendering templates -//! - `OutputFormatObject` - Template-accessible output format renderer -//! - Value conversion from `BexExternalValue` to minijinja values -//! - Magic delimiter handling for chat messages and media - -mod error; -mod output_format_object; -mod render; -mod value_conversion; - -pub use error::RenderPromptError; -pub use render::{ - RenderContext, RenderContextClient, RenderEnum, RenderEnumVariant, preprocess_template, - render_prompt, -}; - -pub use crate::types::OutputFormatContent; - -/// Magic delimiter for chat role markers. -pub(crate) const MAGIC_CHAT_ROLE_DELIMITER: &str = "BAML_CHAT_ROLE_MAGIC_STRING_DELIMITER"; - -/// Magic delimiter for media content. -pub(crate) const MAGIC_MEDIA_DELIMITER: &str = "BAML_MEDIA_MAGIC_STRING_DELIMITER"; diff --git a/baml_language/crates/sys_llm/src/jinja/output_format_object.rs b/baml_language/crates/sys_llm/src/jinja/output_format_object.rs deleted file mode 100644 index 41619463146..00000000000 --- a/baml_language/crates/sys_llm/src/jinja/output_format_object.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! `OutputFormat` object exposed to Jinja templates as `ctx.output_format`. -//! Ported from engine/baml-lib/jinja-runtime/src/output_format/mod.rs - -use std::sync::Arc; - -use minijinja::{ - ErrorKind, - value::{Kwargs, Value}, -}; - -use crate::types::{HoistClasses, MapStyle, OutputFormatContent, RenderOptions, RenderSetting}; - -/// Wrapper around `OutputFormatContent` that implements `minijinja::value::Object`. -#[derive(Debug)] -pub(super) struct OutputFormatObject { - content: OutputFormatContent, -} - -impl OutputFormatObject { - pub(super) fn new(content: OutputFormatContent) -> Self { - Self { content } - } -} - -impl std::fmt::Display for OutputFormatObject { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let rendered = self - .content - .render(&RenderOptions::default()) - .map_err(|_| std::fmt::Error)?; - match rendered { - Some(s) => write!(f, "{s}"), - None => Ok(()), - } - } -} - -impl minijinja::value::Object for OutputFormatObject { - fn call( - self: &Arc, - _state: &minijinja::State<'_, '_>, - args: &[Value], - ) -> Result { - use minijinja::{Error, value::from_args}; - - let (args, kwargs): (&[Value], Kwargs) = from_args(args)?; - if !args.is_empty() { - return Err(Error::new( - ErrorKind::TooManyArguments, - "output_format() may only be called with named arguments", - )); - } - - // Parse kwargs - matching engine/baml-lib/jinja-runtime/src/output_format/mod.rs - let prefix = parse_render_setting_string(&kwargs, "prefix")?; - let or_splitter = parse_render_setting_string(&kwargs, "or_splitter")?; - let enum_value_prefix = parse_render_setting_string(&kwargs, "enum_value_prefix")?; - let always_hoist_enums = parse_render_setting_bool(&kwargs, "always_hoist_enums")?; - let hoisted_class_prefix = parse_render_setting_string(&kwargs, "hoisted_class_prefix")?; - let hoist_classes = parse_hoist_classes_kwarg(&kwargs)?; - let map_style = parse_map_style_kwarg(&kwargs)?; - let quote_class_fields = parse_render_setting_bool(&kwargs, "quote_class_fields")?; - let render_null_as = parse_render_null_as_kwarg(&kwargs, "render_null_as")?; - - kwargs.assert_all_used().map_err(|_| { - Error::new( - ErrorKind::TooManyArguments, - "output_format() got an unexpected keyword argument", - ) - })?; - - let options = RenderOptions { - prefix, - or_splitter, - enum_value_prefix, - hoisted_class_prefix, - hoist_classes, - always_hoist_enums, - map_style, - quote_class_fields, - render_null_as, - }; - - let rendered = self - .content - .render(&options) - .map_err(|e| Error::new(ErrorKind::BadSerialization, e.to_string()))?; - - match rendered { - Some(s) => Ok(Value::from_safe_string(s)), - None => Ok(Value::from("")), - } - } - - fn render(self: &Arc, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } -} - -// Helper functions for parsing kwargs - -/// Parse a kwarg that returns `RenderSetting`: -/// - Not present -> Auto -/// - Present with null/None -> Never -/// - Present with value -> Always(value) -fn parse_render_setting_string( - kwargs: &Kwargs, - name: &str, -) -> Result, minijinja::Error> { - if !kwargs.has(name) { - return Ok(RenderSetting::Auto); - } - match kwargs.get::>(name) { - Ok(Some(v)) => Ok(RenderSetting::Always(v)), - Ok(None) => Ok(RenderSetting::Never), - Err(e) => Err(minijinja::Error::new( - ErrorKind::SyntaxError, - format!("Invalid value for {name}: {e}"), - )), - } -} - -/// Parse the `render_null_as` kwarg: -/// - Not present or null/None -> Auto (`null`) -/// - Present with value -> Always(value) -fn parse_render_null_as_kwarg( - kwargs: &Kwargs, - name: &str, -) -> Result, minijinja::Error> { - if !kwargs.has(name) { - return Ok(RenderSetting::Auto); - } - match kwargs.get::>(name) { - Ok(Some(v)) => Ok(RenderSetting::Always(v)), - Ok(None) => Ok(RenderSetting::Auto), - Err(e) => Err(minijinja::Error::new( - ErrorKind::SyntaxError, - format!("Invalid value for {name}: {e}"), - )), - } -} - -/// Parse a kwarg that returns `RenderSetting`: -/// - Not present -> Auto -/// - Present with value -> Always(value) -fn parse_render_setting_bool( - kwargs: &Kwargs, - name: &str, -) -> Result, minijinja::Error> { - if !kwargs.has(name) { - return Ok(RenderSetting::Auto); - } - match kwargs.get::(name) { - Ok(v) => Ok(RenderSetting::Always(v)), - Err(e) => Err(minijinja::Error::new( - ErrorKind::SyntaxError, - format!("Invalid value for {name}: {e}"), - )), - } -} - -/// Parse `hoist_classes` kwarg: -/// - Not present -> Auto -/// - true -> All -/// - false -> Auto -/// - "auto" -> Auto -/// - string[] -> Subset(classes) -fn parse_hoist_classes_kwarg(kwargs: &Kwargs) -> Result { - if !kwargs.has("hoist_classes") { - return Ok(HoistClasses::Auto); - } - // Try bool first - if let Ok(b) = kwargs.get::("hoist_classes") { - return Ok(if b { - HoistClasses::All - } else { - HoistClasses::Auto - }); - } - // Try "auto" string - if let Ok(s) = kwargs.get::("hoist_classes") { - if s == "auto" { - return Ok(HoistClasses::Auto); - } - } - // Try array of strings - if let Ok(classes) = kwargs.get::>("hoist_classes") { - return Ok(HoistClasses::Subset(classes)); - } - Err(minijinja::Error::new( - ErrorKind::SyntaxError, - "Invalid value for hoist_classes (expected bool | \"auto\" | string[])", - )) -} - -/// Parse `map_style` kwarg: -/// - Not present -> `ObjectLiteral` (default JSON object shape) -/// - "`type_parameters`" -> `TypeParameters` (opt-in `map` escape hatch) -/// - "`object_literal`" -> `ObjectLiteral` -fn parse_map_style_kwarg(kwargs: &Kwargs) -> Result { - if !kwargs.has("map_style") { - return Ok(MapStyle::default()); - } - match kwargs.get::("map_style") { - Ok(s) => match s.as_str() { - "type_parameters" => Ok(MapStyle::TypeParameters), - "object_literal" => Ok(MapStyle::ObjectLiteral), - _ => Err(minijinja::Error::new( - ErrorKind::SyntaxError, - format!("Invalid map_style: {s} (expected 'type_parameters' or 'object_literal')"), - )), - }, - Err(e) => Err(minijinja::Error::new( - ErrorKind::SyntaxError, - format!("Invalid value for map_style: {e}"), - )), - } -} diff --git a/baml_language/crates/sys_llm/src/jinja/render.rs b/baml_language/crates/sys_llm/src/jinja/render.rs deleted file mode 100644 index a2e41edcf80..00000000000 --- a/baml_language/crates/sys_llm/src/jinja/render.rs +++ /dev/null @@ -1,860 +0,0 @@ -use std::collections::HashMap; - -use baml_builtins2::{PromptAst, PromptAstSimple}; -use bex_external_types::BexExternalValue; -use indexmap::IndexMap; -use minijinja::Environment; -use sys_jinja::create_environment; - -use super::{ - MAGIC_CHAT_ROLE_DELIMITER, MAGIC_MEDIA_DELIMITER, output_format_object::OutputFormatObject, - value_conversion::external_value_to_jinja, -}; -use crate::types::OutputFormatContent; - -/// Enum variant for Jinja rendering. -#[derive(Clone, Debug)] -pub struct RenderEnumVariant { - pub name: String, -} - -/// Enum definition for Jinja rendering. -#[derive(Clone, Debug)] -pub struct RenderEnum { - pub name: String, - pub variants: Vec, -} - -/// Client configuration for rendering. -#[derive(Clone, Debug)] -pub struct RenderContextClient { - pub name: String, - pub provider: String, - pub default_role: String, - pub allowed_roles: Vec, -} - -/// Context for rendering a prompt. -#[derive(Clone, Debug)] -pub struct RenderContext { - pub client: RenderContextClient, - pub output_format: OutputFormatContent, - pub tags: IndexMap, - /// Enum definitions available in templates. - /// Each enum is accessible as a global, e.g., `{{ MyEnum.VALUE }}`. - pub enums: HashMap, -} - -/// Render a Jinja template to a `PromptAst`. -/// -/// # Arguments -/// * `template` - The Jinja template string -/// * `args` - Template arguments as pre-extracted `BexExternalValue` (no heap access needed) -/// * `ctx` - Rendering context with client info and output format -/// -/// # Returns -/// A `PromptAst` representing the rendered prompt. -pub fn render_prompt( - template: &str, - args: &IndexMap, - ctx: &RenderContext, -) -> Result { - let mut env = create_environment(); - - env.add_template("prompt", template)?; - - let mut media_handles = HashMap::new(); - // Add globals (tags may contain media; share media_handles so parse_rendered_output can resolve them) - add_globals(&mut env, ctx, &mut media_handles)?; - - // Build context - args are already extracted BexExternalValue - let jinja_args: minijinja::value::Value = args - .iter() - .map(|(k, v)| Ok((k.clone(), external_value_to_jinja(v, &mut media_handles)?))) - .collect::>()?; - let tmpl = env.get_template("prompt")?; - - // Render - let rendered = tmpl.render(jinja_args)?; - - // Parse result into PromptAst - Ok(parse_rendered_output(&rendered, ctx, &media_handles)) -} - -/// Preprocess template: dedent and trim. -/// -/// Dedenting logic ported from engine/baml-lib/jinja-runtime/src/lib.rs:266-277 -pub fn preprocess_template(template: &str) -> String { - // Dedent: find minimum whitespace and remove from all lines - let lines: Vec<&str> = template.lines().collect(); - - let min_indent = lines - .iter() - .filter(|line| !line.trim().is_empty()) - .map(|line| line.len() - line.trim_start().len()) - .min() - .unwrap_or(0); - - lines - .iter() - .map(|line| { - if line.len() >= min_indent { - &line[min_indent..] - } else { - line.trim() - } - }) - .collect::>() - .join("\n") - .trim() - .to_string() -} - -fn add_globals( - env: &mut Environment, - ctx: &RenderContext, - media_handles: &mut HashMap, -) -> Result<(), super::RenderPromptError> { - use minijinja::context; - - // Create role function - same function used for both _.role() and _.chat() - // Ported from engine/baml-lib/jinja-runtime/src/lib.rs:382-387 - let default_role = ctx.client.default_role.clone(); - let role_fn = minijinja::value::Value::from_function(move |role: Option| -> String { - let r = role.unwrap_or_else(|| default_role.clone()); - format!( - "{MAGIC_CHAT_ROLE_DELIMITER}:baml-start-role:{r}:baml-end-role:{MAGIC_CHAT_ROLE_DELIMITER}" - ) - }); - - // Add _ namespace with chat and role functions - env.add_global( - "_", - context! { - chat => role_fn, - role => role_fn, - }, - ); - - // Build enums map - each enum is accessible as {{ ctx.enums.EnumName.VARIANT }} - let enums_map: minijinja::value::Value = ctx - .enums - .iter() - .map(|(name, def)| { - let variants: IndexMap = def - .variants - .iter() - .map(|v| { - ( - v.name.clone(), - minijinja::value::Value::from(v.name.clone()), - ) - }) - .collect(); - (name.clone(), minijinja::value::Value::from_iter(variants)) - }) - .collect(); - - // Add ctx namespace with output_format and enums - // Ported from engine/baml-lib/jinja-runtime/src/output_format/mod.rs - let output_format = OutputFormatObject::new(ctx.output_format.clone()); - env.add_global( - "ctx", - context! { - client => context! { - name => ctx.client.name.clone(), - provider => ctx.client.provider.clone(), - }, - tags => ctx.tags.iter().map(|(k, v)| Ok((k.clone(), external_value_to_jinja(v, media_handles)?))).collect::, super::RenderPromptError>>()?.into_iter().collect::(), - output_format => minijinja::value::Value::from_object(output_format), - enums => enums_map, - }, - ); - Ok(()) -} - -fn parse_rendered_output( - rendered: &str, - ctx: &RenderContext, - media_handles: &HashMap, -) -> PromptAst { - // Check if this is a chat-style prompt (contains role delimiters) - if rendered.contains(MAGIC_CHAT_ROLE_DELIMITER) { - parse_chat_prompt(rendered, ctx, media_handles) - } else if rendered.contains(MAGIC_MEDIA_DELIMITER) { - let content = parse_message_content(rendered, media_handles); - PromptAst::Simple(std::sync::Arc::new(content)) - } else { - rendered.to_string().into() - } -} - -fn parse_chat_prompt( - rendered: &str, - _ctx: &RenderContext, - media_handles: &HashMap, -) -> PromptAst { - let mut messages = Vec::new(); - - // Split on role delimiter - let parts: Vec<&str> = rendered.split(MAGIC_CHAT_ROLE_DELIMITER).collect(); - - let mut current_role: Option = None; - let mut current_content = String::new(); - - for part in parts { - if part.starts_with(":baml-start-role:") && part.ends_with(":baml-end-role:") { - // Save previous message if any - if let Some(role) = current_role.take() { - let content = parse_message_content(¤t_content, media_handles); - messages.push(PromptAst::Message { - role, - content: content.into(), - metadata: serde_json::Value::default(), - }); - current_content.clear(); - } - - // Extract new role - let role = part - .strip_prefix(":baml-start-role:") - .and_then(|s| s.strip_suffix(":baml-end-role:")) - .unwrap_or("user") - .to_string(); - current_role = Some(role); - } else { - current_content.push_str(part); - } - } - - // Save last message - if let Some(role) = current_role { - let content = parse_message_content(¤t_content, media_handles); - messages.push(PromptAst::Message { - role, - content: std::sync::Arc::new(content), - metadata: serde_json::Value::default(), - }); - } - - if messages.is_empty() { - rendered.to_string().into() - } else if messages.len() == 1 { - messages.pop().unwrap() - } else { - PromptAst::Vec(messages.into_iter().map(std::sync::Arc::new).collect()) - } -} - -fn parse_message_content( - content: &str, - media_handles: &HashMap, -) -> PromptAstSimple { - // Check for media delimiters - if content.contains(MAGIC_MEDIA_DELIMITER) { - let mut parts = Vec::new(); - let chunks: Vec<&str> = content.split(MAGIC_MEDIA_DELIMITER).collect(); - - for (i, chunk) in chunks.iter().enumerate() { - if i % 2 == 1 { - // This is a media chunk - parse the handle - // Format: :baml-start-media:{handle}:baml-end-media: - if let Some(handle) = parse_media_handle(chunk) { - if let Some(media) = media_handles.get(&handle) { - parts.push(PromptAstSimple::Media(media.clone())); - } else { - // Handle not found (e.g. mismatched delimiter); treat as literal string - parts.push(PromptAstSimple::String("[media not found]".to_string())); - } - } - } else if !chunk.trim().is_empty() { - parts.push(PromptAstSimple::String((*chunk).trim().to_string())); - } - } - - if parts.len() == 1 { - parts.pop().unwrap() - } else { - PromptAstSimple::Multiple(parts.into_iter().map(std::sync::Arc::new).collect()) - } - } else { - PromptAstSimple::String(content.trim().to_string()) - } -} - -fn parse_media_handle(chunk: &str) -> Option { - // Extract handle from format: :baml-start-media:{handle}:baml-end-media: - chunk - .strip_prefix(":baml-start-media:") - .and_then(|s| s.strip_suffix(":baml-end-media:")) - .and_then(|s| s.parse().ok()) -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use baml_type::RuntimeTy; - - use super::*; - - fn test_ctx() -> RenderContext { - RenderContext { - client: RenderContextClient { - name: "test".to_string(), - provider: "openai".to_string(), - default_role: "user".to_string(), - allowed_roles: vec![ - "user".to_string(), - "assistant".to_string(), - "system".to_string(), - ], - }, - output_format: OutputFormatContent::new(RuntimeTy::String { - attr: baml_type::TyAttr::default(), - }), - tags: IndexMap::new(), - enums: HashMap::new(), - } - } - - #[test] - fn test_simple_string() { - let template = "Hello, world!"; - let args = IndexMap::new(); - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - assert_eq!(result, "Hello, world!".to_string().into()); - } - - #[test] - fn test_variable_substitution() { - let template = "Hello, {{ name }}!"; - - let mut args = IndexMap::new(); - args.insert("name".to_string(), BexExternalValue::String("Alice".into())); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - assert_eq!(result, "Hello, Alice!".to_string().into()); - } - - #[test] - fn test_nested_object() { - let template = "Name: {{ person.name }}, Age: {{ person.age }}"; - - let mut person_fields = IndexMap::new(); - person_fields.insert("name".to_string(), BexExternalValue::String("Bob".into())); - person_fields.insert("age".to_string(), BexExternalValue::Int(30)); - - let mut args = IndexMap::new(); - args.insert( - "person".to_string(), - BexExternalValue::Instance { - class_name: "Person".to_string(), - type_args: vec![], - fields: person_fields, - }, - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - assert_eq!(result, "Name: Bob, Age: 30".to_string().into()); - } - - #[test] - fn test_chat_with_roles() { - let template = r#" - {{ _.role("system") }} - You are a helpful assistant. - {{ _.role("user") }} - Hello! - "#; - let args = IndexMap::new(); - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - let expected = PromptAst::Vec( - vec![ - PromptAst::Message { - role: "system".to_string(), - content: Arc::new(("You are a helpful assistant.".to_string()).into()), - metadata: serde_json::Value::default(), - }, - PromptAst::Message { - role: "user".to_string(), - content: Arc::new("Hello!".to_string().into()), - metadata: serde_json::Value::default(), - }, - ] - .into_iter() - .map(std::sync::Arc::new) - .collect(), - ); - - assert_eq!(result, expected); - } - - #[test] - fn test_chat_default_role() { - let template = r#" - {{ _.chat() }} - Hello with default role! - "#; - let args = IndexMap::new(); - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - let expected = PromptAst::Message { - role: "user".to_string(), - content: Arc::new("Hello with default role!".to_string().into()), - metadata: serde_json::Value::default(), - }; - - assert_eq!(result, expected); - } - - #[test] - fn test_dedent() { - let template = r#" - Hello, - World! - "#; - let result = preprocess_template(template); - assert_eq!(result, "Hello,\nWorld!"); - } - - #[test] - fn test_array_iteration() { - let template = "Items: {% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}"; - - let mut args = IndexMap::new(); - args.insert( - "items".to_string(), - BexExternalValue::Array { - element_type: RuntimeTy::String { - attr: baml_type::TyAttr::default(), - }, - items: vec![ - BexExternalValue::String("apple".into()), - BexExternalValue::String("banana".into()), - BexExternalValue::String("cherry".into()), - ], - }, - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - assert_eq!(result, "Items: apple, banana, cherry".to_string().into()); - } - - #[test] - fn test_output_format_in_template() { - let template = "{{ ctx.output_format }}"; - let args = IndexMap::new(); - - // Create a context with an int output format - let mut ctx = test_ctx(); - ctx.output_format = OutputFormatContent::new(RuntimeTy::Int { - attr: baml_type::TyAttr::default(), - }); - - let result = render_prompt(template, &args, &ctx).unwrap(); - - assert_eq!(result, "Answer as an int".to_string().into()); - } - - #[test] - fn test_output_format_with_kwargs() { - let template = "{{ ctx.output_format(prefix='Please respond with: ') }}"; - let args = IndexMap::new(); - - let mut ctx = test_ctx(); - ctx.output_format = OutputFormatContent::new(RuntimeTy::Int { - attr: baml_type::TyAttr::default(), - }); - - let result = render_prompt(template, &args, &ctx).unwrap(); - - assert_eq!(result, "Please respond with: int".to_string().into()); - } - - #[test] - fn test_format_number_with_commas() { - let template = r#"{{ "{:,}".format(1234567) }}"#; - let args = IndexMap::new(); - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - assert_eq!(result, "1,234,567".to_string().into()); - - // float formatting - let template = r#"{{ "{:.2f}".format(3.14159) }}"#; - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - assert_eq!(result, "3.14".to_string().into()); - - // negative integers - let template = r#"{{ "{:,}".format(-1234567) }}"#; - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - assert_eq!(result, "-1,234,567".to_string().into()); - } - - // ======================================================================== - // Media tests - // ======================================================================== - - fn make_media( - kind: baml_base::MediaKind, - content: baml_builtins2::MediaContent, - mime: Option<&str>, - ) -> std::sync::Arc { - std::sync::Arc::new(baml_builtins2::MediaValue::new( - kind, - content, - mime.map(String::from), - )) - } - - fn make_media_external( - kind: baml_base::MediaKind, - content: baml_builtins2::MediaContent, - mime: Option<&str>, - ) -> BexExternalValue { - BexExternalValue::Adt(bex_external_types::BexExternalAdt::Media(make_media( - kind, content, mime, - ))) - } - - fn make_media_instance( - class_name: &str, - kind: baml_base::MediaKind, - content: baml_builtins2::MediaContent, - mime: Option<&str>, - ) -> BexExternalValue { - let media = make_media_external(kind, content, mime); - let mut fields = IndexMap::new(); - fields.insert("_data".to_string(), media); - BexExternalValue::Instance { - class_name: class_name.to_string(), - type_args: vec![], - fields, - } - } - - #[test] - fn test_media_renders_as_prompt_ast_media() { - let template = "Describe this: {{ img }}"; - let mut args = IndexMap::new(); - args.insert( - "img".to_string(), - make_media_external( - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Url { - url: "https://example.com/photo.jpg".into(), - base64_data: None, - }, - Some("image/jpeg"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - // Should produce a Multiple with text + media - match &result { - PromptAst::Simple(content) => match content.as_ref() { - PromptAstSimple::Multiple(parts) => { - assert_eq!(parts.len(), 2); - match parts[0].as_ref() { - PromptAstSimple::String(s) => assert_eq!(s, "Describe this:"), - other => panic!("Expected String, got {other:?}"), - } - match parts[1].as_ref() { - PromptAstSimple::Media(m) => { - assert_eq!(m.kind, baml_base::MediaKind::Image); - } - other => panic!("Expected Media, got {other:?}"), - } - } - other => panic!("Expected Multiple, got {other:?}"), - }, - other => panic!("Expected Simple, got {other:?}"), - } - } - - #[test] - fn test_media_in_chat_message() { - let template = r#" - {{ _.role("user") }} - Look at this image: - {{ img }} - "#; - - let mut args = IndexMap::new(); - args.insert( - "img".to_string(), - make_media_external( - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Base64 { - base64_data: "iVBORw0KGgo=".into(), - }, - Some("image/png"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - match &result { - PromptAst::Message { role, content, .. } => { - assert_eq!(role, "user"); - match content.as_ref() { - PromptAstSimple::Multiple(parts) => { - assert_eq!(parts.len(), 2); - assert!(matches!(parts[0].as_ref(), PromptAstSimple::String(_))); - assert!(matches!(parts[1].as_ref(), PromptAstSimple::Media(_))); - } - other => panic!("Expected Multiple, got {other:?}"), - } - } - other => panic!("Expected Message, got {other:?}"), - } - } - - #[test] - fn test_media_wrapper_instance_unwrapped() { - let template = "{{ receipt }}"; - let mut args = IndexMap::new(); - args.insert( - "receipt".to_string(), - make_media_instance( - "baml.media.Image", - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Url { - url: "https://example.com/receipt.jpg".into(), - base64_data: None, - }, - Some("image/jpeg"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - // The wrapper instance should be unwrapped to produce a media node - match &result { - PromptAst::Simple(content) => match content.as_ref() { - PromptAstSimple::Media(m) => { - assert_eq!(m.kind, baml_base::MediaKind::Image); - } - other => panic!("Expected Media, got {other:?}"), - }, - other => panic!("Expected Simple, got {other:?}"), - } - } - - #[test] - fn test_media_wrapper_audio() { - let template = "{{ audio }}"; - let mut args = IndexMap::new(); - args.insert( - "audio".to_string(), - make_media_instance( - "baml.media.Audio", - baml_base::MediaKind::Audio, - baml_builtins2::MediaContent::Base64 { - base64_data: "AAAA".into(), - }, - Some("audio/wav"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - match &result { - PromptAst::Simple(content) => match content.as_ref() { - PromptAstSimple::Media(m) => { - assert_eq!(m.kind, baml_base::MediaKind::Audio); - } - other => panic!("Expected Media, got {other:?}"), - }, - other => panic!("Expected Simple, got {other:?}"), - } - } - - #[test] - fn test_media_wrapper_pdf() { - let template = "{{ doc }}"; - let mut args = IndexMap::new(); - args.insert( - "doc".to_string(), - make_media_instance( - "baml.media.Pdf", - baml_base::MediaKind::Pdf, - baml_builtins2::MediaContent::Url { - url: "https://example.com/doc.pdf".into(), - base64_data: None, - }, - Some("application/pdf"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - match &result { - PromptAst::Simple(content) => match content.as_ref() { - PromptAstSimple::Media(m) => { - assert_eq!(m.kind, baml_base::MediaKind::Pdf); - } - other => panic!("Expected Media, got {other:?}"), - }, - other => panic!("Expected Simple, got {other:?}"), - } - } - - #[test] - fn test_media_with_text_in_chat() { - let template = r#" - {{ _.role("user") }} - Extract info from this receipt: - {{ receipt }} - {{ ctx.output_format }} - "#; - - let mut args = IndexMap::new(); - args.insert( - "receipt".to_string(), - make_media_external( - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Url { - url: "https://example.com/receipt.jpg".into(), - base64_data: None, - }, - Some("image/jpeg"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - match &result { - PromptAst::Message { role, content, .. } => { - assert_eq!(role, "user"); - match content.as_ref() { - PromptAstSimple::Multiple(parts) => { - // Should have: text, media (output_format for string is empty) - assert!( - parts.len() >= 2, - "Expected at least 2 parts, got {}", - parts.len() - ); - assert!(matches!(parts[0].as_ref(), PromptAstSimple::String(_))); - assert!(matches!(parts[1].as_ref(), PromptAstSimple::Media(_))); - } - other => panic!("Expected Multiple, got {other:?}"), - } - } - other => panic!("Expected Message, got {other:?}"), - } - } - - #[test] - fn test_multiple_media_in_message() { - let template = r#" - {{ _.role("user") }} - Compare these images: - {{ img1 }} - {{ img2 }} - "#; - - let mut args = IndexMap::new(); - args.insert( - "img1".to_string(), - make_media_external( - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Url { - url: "https://example.com/a.jpg".into(), - base64_data: None, - }, - Some("image/jpeg"), - ), - ); - args.insert( - "img2".to_string(), - make_media_external( - baml_base::MediaKind::Image, - baml_builtins2::MediaContent::Url { - url: "https://example.com/b.jpg".into(), - base64_data: None, - }, - Some("image/png"), - ), - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - match &result { - PromptAst::Message { content, .. } => match content.as_ref() { - PromptAstSimple::Multiple(parts) => { - let media_count = parts - .iter() - .filter(|p| matches!(p.as_ref(), PromptAstSimple::Media(_))) - .count(); - assert_eq!(media_count, 2, "Should have 2 media parts"); - } - other => panic!("Expected Multiple, got {other:?}"), - }, - other => panic!("Expected Message, got {other:?}"), - } - } - - #[test] - fn test_non_media_instance_not_unwrapped() { - let template = "{{ person }}"; - let mut fields = IndexMap::new(); - fields.insert("name".to_string(), BexExternalValue::String("Alice".into())); - let mut args = IndexMap::new(); - args.insert( - "person".to_string(), - BexExternalValue::Instance { - class_name: "user.Person".to_string(), - type_args: vec![], - fields, - }, - ); - - let result = render_prompt(template, &args, &test_ctx()).unwrap(); - - // Non-media instances should render as maps, not be unwrapped - match &result { - PromptAst::Simple(content) => match content.as_ref() { - PromptAstSimple::String(s) => { - assert!(s.contains("Alice"), "Should contain field value: {s}"); - } - other => panic!("Expected String, got {other:?}"), - }, - other => panic!("Expected Simple, got {other:?}"), - } - } - - #[test] - fn test_enum_access() { - let template = "Category: {{ ctx.enums.Category.SPORTS }}"; - let args = IndexMap::new(); - - let mut ctx = test_ctx(); - ctx.enums.insert( - "Category".to_string(), - RenderEnum { - name: "Category".to_string(), - variants: vec![ - RenderEnumVariant { - name: "SPORTS".to_string(), - }, - RenderEnumVariant { - name: "TECH".to_string(), - }, - RenderEnumVariant { - name: "POLITICS".to_string(), - }, - ], - }, - ); - - let result = render_prompt(template, &args, &ctx).unwrap(); - - assert_eq!(result, "Category: SPORTS".to_string().into()); - } -} diff --git a/baml_language/crates/sys_llm/src/jinja/value_conversion.rs b/baml_language/crates/sys_llm/src/jinja/value_conversion.rs deleted file mode 100644 index 207528a4765..00000000000 --- a/baml_language/crates/sys_llm/src/jinja/value_conversion.rs +++ /dev/null @@ -1,128 +0,0 @@ -use bex_external_types::{BexExternalAdt, BexExternalValue}; -use bex_vm_types::MediaValue; -use indexmap::IndexMap; -use minijinja::value::Value as JinjaValue; - -use super::{MAGIC_MEDIA_DELIMITER, RenderPromptError}; - -/// Convert a `BexExternalValue` to a minijinja Value. -/// -/// `BexExternalValue` is already fully extracted from the VM heap, -/// so no heap access is needed here. -pub(crate) fn external_value_to_jinja( - value: &BexExternalValue, - media_handles: &mut std::collections::HashMap, -) -> Result { - match value { - BexExternalValue::Handle(_) => Err(RenderPromptError::ConversionError { - reason: "Handle should not be passed to Jinja templates".to_string(), - }), - BexExternalValue::Null => Ok(JinjaValue::from(())), // Maps to None in Jinja - BexExternalValue::Int(i) => Ok(JinjaValue::from(*i)), - // Arbitrary-precision bigints can exceed Jinja's `i128`/`u128` - // representations; pass them through as decimal strings so prompts - // see a faithful numeric rendering rather than a truncation. - BexExternalValue::Bigint(b) => Ok(JinjaValue::from(b.to_string())), - BexExternalValue::Float(f) => Ok(JinjaValue::from(*f)), - BexExternalValue::Bool(b) => Ok(JinjaValue::from(*b)), - BexExternalValue::String(s) => Ok(JinjaValue::from(s.as_str())), - - BexExternalValue::Array { items, .. } => { - let jinja_items: Vec = items - .iter() - .map(|item| external_value_to_jinja(item, media_handles)) - .collect::>()?; - Ok(JinjaValue::from(jinja_items)) - } - - BexExternalValue::Map { entries, .. } => { - let jinja_map: IndexMap = entries - .iter() - .map(|(k, v)| Ok((k.clone(), external_value_to_jinja(v, media_handles)?))) - .collect::>()?; - Ok(JinjaValue::from_iter(jinja_map)) - } - - BexExternalValue::Instance { - class_name, fields, .. - } => { - // Media wrapper instances (e.g. baml.media.Image) should be unwrapped - // to their structural payload and rendered as inline media content. - if value.media_wrapper_kind().is_some() { - let data = value.media_wrapper_inner().ok_or_else(|| { - RenderPromptError::ConversionError { - reason: format!( - "Media wrapper `{class_name}` missing {} field", - bex_external_types::MEDIA_WRAPPER_DATA_FIELD - ), - } - })?; - return external_value_to_jinja(data, media_handles); - } - // Convert instance fields to a map for Jinja access - let jinja_map: IndexMap = fields - .iter() - .map(|(k, v)| Ok((k.clone(), external_value_to_jinja(v, media_handles)?))) - .collect::>()?; - Ok(JinjaValue::from_iter(jinja_map)) - } - - BexExternalValue::Variant { - variant_name, - enum_name: _, - } => { - // Enum variants are rendered as their variant name - Ok(JinjaValue::from(variant_name.as_str())) - } - - BexExternalValue::Union { value, .. } => { - // Unwrap the union and convert the inner value - external_value_to_jinja(value, media_handles) - } - - BexExternalValue::Adt(BexExternalAdt::Media(media)) => { - let media_id = media.random_id; - media_handles.insert(media_id, media.clone()); - Ok(JinjaValue::from(format!( - "{MAGIC_MEDIA_DELIMITER}:baml-start-media:{media_id}:baml-end-media:{MAGIC_MEDIA_DELIMITER}" - ))) - } - - BexExternalValue::Uint8Array(_) => Err(RenderPromptError::ConversionError { - reason: "uint8array cannot be passed to Jinja templates".to_string(), - }), - BexExternalValue::RustData(_) => Err(RenderPromptError::ConversionError { - reason: "RustData should not be passed to Jinja templates".to_string(), - }), - - BexExternalValue::Adt(BexExternalAdt::Collector(_)) => { - Err(RenderPromptError::ConversionError { - reason: "Collector should not be passed to Jinja templates".to_string(), - }) - } - - BexExternalValue::Adt(BexExternalAdt::Type(_)) => Err(RenderPromptError::ConversionError { - reason: "Type values cannot be passed to Jinja templates".to_string(), - }), - - BexExternalValue::Adt(BexExternalAdt::PromptAst(_)) => { - Err(RenderPromptError::ConversionError { - reason: "PromptAst should not be passed to Jinja templates".to_string(), - }) - } - - BexExternalValue::Adt(BexExternalAdt::TaggedHeapHandle { .. }) => { - Err(RenderPromptError::ConversionError { - reason: "tagged heap handle should not be passed to Jinja templates".to_string(), - }) - } - - BexExternalValue::FunctionRef { .. } => Err(RenderPromptError::ConversionError { - reason: "FunctionRef should not be passed to Jinja templates".to_string(), - }), - - BexExternalValue::HostValue(_) => Err(RenderPromptError::ConversionError { - reason: "HostValue should not be passed to Jinja templates".to_string(), - }), - } -} diff --git a/baml_language/crates/sys_llm/src/lib.rs b/baml_language/crates/sys_llm/src/lib.rs index 27edcde35ca..6572a857264 100644 --- a/baml_language/crates/sys_llm/src/lib.rs +++ b/baml_language/crates/sys_llm/src/lib.rs @@ -2,14 +2,12 @@ //! //! This crate consolidates all LLM-related functionality: //! - `types` - Error types and output format schema types -//! - `jinja` - Jinja template rendering for BAML prompts //! - `specialize_prompt()` - Transform a generic `PromptAst` for a specific LLM provider //! - `execute_*` entry points for trait-based dispatch from `sys_types` mod auth_request; pub mod baml_std; mod build_request; -pub(crate) mod jinja; mod model_features; pub(crate) mod parse_response; mod provider; @@ -21,19 +19,13 @@ pub(crate) mod types; use std::{str::FromStr, sync::Arc}; use bex_external_types::BexExternalValue; -// Used by bex_engine tests -pub use jinja::{ - OutputFormatContent, RenderContext, RenderContextClient, RenderEnum, RenderEnumVariant, - RenderPromptError, preprocess_template, render_prompt, -}; // --- Crate-internal re-exports (used by submodules via `crate::`) --- pub(crate) use model_features::{AllowedMetadata, ModelFeatures}; // Used by sys_types (From for VmBamlError) pub use provider::LlmProvider; -pub use sys_jinja::undeclared_prompt_variables; -pub use types::LlmOpError; // --- Public API: only what sys_types and bex_engine tests actually use --- pub use types::SapParseCache; +pub use types::{LlmOpError, OutputFormatContent}; // Selects the rustls crypto provider for the crate. No longer called directly // now that all HTTPS flows through `RuntimeIo` (sys_native installs its own @@ -68,50 +60,9 @@ pub(crate) fn ensure_rustls_crypto_provider() {} // Clean (owned-type) entry points for trait-based dispatch // ============================================================================ -/// Render a Jinja template given already-extracted owned types. -/// -/// `args` is expected to be `BexExternalValue::Map { entries, .. }`. -pub fn execute_render_prompt_from_owned( - client: &baml_std::PrimitiveClient, - template: &str, - args: &BexExternalValue, - return_type: &baml_type::RuntimeTy, - ctx: &::sys_types::SysOpContext, -) -> Result { - let BexExternalValue::Map { - entries: template_args, - .. - } = args - else { - return Err(LlmOpError::TypeError { - expected: "map", - actual: args.type_name().to_string(), - }); - }; - - let output_format = build_output_format_content(return_type, ctx); - - let render_ctx = jinja::RenderContext { - client: jinja::RenderContextClient { - name: client.name.clone(), - provider: client.provider.clone(), - default_role: client.default_role.clone(), - allowed_roles: client.allowed_roles.clone(), - }, - output_format, - tags: indexmap::IndexMap::new(), - enums: std::collections::HashMap::new(), - }; - - let prompt_ast = jinja::render_prompt(template, template_args, &render_ctx) - .map_err(|e| LlmOpError::RenderPrompt(e.to_string()))?; - Ok(std::sync::Arc::new(prompt_ast)) -} - -/// BEP-049 §10 (M5b). Render `return_type`'s schema with default options — the -/// string a `prompt` body reads as `ctx.output_format`. Equivalent to the Jinja -/// path's `{{ ctx.output_format }}` (`OutputFormatObject`'s `Display`): build the -/// `OutputFormatContent`, render with `RenderOptions::default()`. An empty/`None` +/// Render `return_type`'s schema with default options for +/// `ctx.output_format`. Build the `OutputFormatContent`, then render with +/// `RenderOptions::default()`. An empty or `None` /// render (e.g. a primitive return type with no schema) becomes the empty string. pub fn render_output_format( return_type: &baml_type::RuntimeTy, @@ -124,9 +75,8 @@ pub fn render_output_format( .unwrap_or_default() } -/// BEP-049 §10 (M5b.2). Render a prebuilt [`OutputFormatContent`] with -/// caller-supplied options — backs the parameterized `ctx.output_format_with(...)` -/// accessor (the rare `{{ ctx.output_format(...) }}` Jinja form). The content is +/// Render a prebuilt [`OutputFormatContent`] with caller-supplied options. +/// This backs the parameterized `ctx.output_format_with(...)` accessor. The content is /// carried as an opaque handle on `Context` (built once by /// [`build_output_format_content`]); this only re-renders. The option mapping /// (`RenderSetting`/`RenderOptions`) stays crate-internal: a value overrides @@ -1723,10 +1673,7 @@ mod tests { #[test] fn render_output_format_renders_class_schema() { - // BEP-049 M5b: `render_output_format` is the `ctx.output_format` backing. - // For a class return type it must emit the schema (field names), matching - // what `OutputFormatObject`'s `Display` (the Jinja `{{ ctx.output_format }}`) - // produces from the same `OutputFormatContent`. + // For a class return type, `ctx.output_format` must emit the schema. let ctx = ctx_with( vec![( tn("User"), diff --git a/baml_language/crates/sys_types/src/lib.rs b/baml_language/crates/sys_types/src/lib.rs index aec2d377fb8..4681ca4f4e6 100644 --- a/baml_language/crates/sys_types/src/lib.rs +++ b/baml_language/crates/sys_types/src/lib.rs @@ -610,10 +610,6 @@ pub struct SysOpContext>, - /// Pre-formatted Jinja `{% macro %}` definitions for all `template_strings`. - /// Prepended to templates by `get_jinja_template`. - pub template_strings_macros: Arc, - /// Per-call cancellation token. /// /// Defaults to a never-cancelled token for the shared engine context. @@ -646,7 +642,6 @@ impl Clone for SysOpContext { Self { llm_functions: self.llm_functions.clone(), function_global_indices: self.function_global_indices.clone(), - template_strings_macros: self.template_strings_macros.clone(), cancel: self.cancel.clone(), class_definitions: self.class_definitions.clone(), enum_definitions: self.enum_definitions.clone(), @@ -669,10 +664,6 @@ pub struct EngineSysOpContext { /// Used by `resolve_client` to return `FunctionRef` values. pub function_global_indices: Arc>, - /// Pre-formatted Jinja `{% macro %}` definitions for all `template_strings`. - /// Prepended to templates by `get_jinja_template`. - pub template_strings_macros: Arc, - /// Pre-extracted class definitions for output format rendering. /// Keyed by class name. pub class_definitions: Arc>, @@ -695,8 +686,6 @@ pub struct EngineSysOpContext { /// This is built during engine construction by reading function objects from the heap, /// so that LLM `sys_ops` don't need to access raw heap pointers. pub struct LlmFunctionInfo { - /// The Jinja prompt template for this function. - pub prompt_template: String, /// The client name (e.g., `"MyClient"`) declared in the function. pub client_name: String, /// The expected return type, used for response parsing. @@ -770,7 +759,6 @@ impl SysOpContext { Self { llm_functions: Arc::new(std::collections::HashMap::new()), function_global_indices: Arc::new(std::collections::HashMap::new()), - template_strings_macros: Arc::new(String::new()), cancel: CancellationToken::new(), class_definitions: Arc::new( indexmap::IndexMap::::new(), @@ -798,7 +786,6 @@ impl EngineSysOpContext { SysOpContext { llm_functions: self.llm_functions.clone(), function_global_indices: self.function_global_indices.clone(), - template_strings_macros: self.template_strings_macros.clone(), cancel, class_definitions: self.class_definitions.clone(), enum_definitions: self.enum_definitions.clone(), From 785b4975656bb94fc30d9540a8bcd7f5d04f36cd Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 19:15:50 -0700 Subject: [PATCH 2/6] update surface snapshot --- ...tests__user_surface_lists_every_kind_with_spans_and_docs.snap | 1 - 1 file changed, 1 deletion(-) diff --git a/baml_language/crates/baml_surface/src/snapshots/baml_surface__handles_tests__user_surface_lists_every_kind_with_spans_and_docs.snap b/baml_language/crates/baml_surface/src/snapshots/baml_surface__handles_tests__user_surface_lists_every_kind_with_spans_and_docs.snap index f063a0ec182..e28369fbd2f 100644 --- a/baml_language/crates/baml_surface/src/snapshots/baml_surface__handles_tests__user_surface_lists_every_kind_with_spans_and_docs.snap +++ b/baml_language/crates/baml_surface/src/snapshots/baml_surface__handles_tests__user_surface_lists_every_kind_with_spans_and_docs.snap @@ -9,6 +9,5 @@ namespace [] interface Renderer span:Renderer doc: Renderer docs. class Widget span:Widget doc: Widget docs. class Widget$stream span: doc: Widget docs. [synthetic] - template_string Prompt span:Prompt doc: - function greet span:greet doc: - test greet_works span:greet_works doc: - From 435ea73766fec7276b2d3f6fe8e3ee01090b6bb6 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 19:17:01 -0700 Subject: [PATCH 3/6] bump bytecode cache format --- baml_language/crates/bex_cache/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/baml_language/crates/bex_cache/src/lib.rs b/baml_language/crates/bex_cache/src/lib.rs index 13d6f00f608..d9808559f3d 100644 --- a/baml_language/crates/bex_cache/src/lib.rs +++ b/baml_language/crates/bex_cache/src/lib.rs @@ -49,7 +49,10 @@ use sha2::{Digest, Sha256}; /// (BEP-062 `reflect.signature`). /// /// Version 3: diagnostic cache blobs gained `message_highlights` fields. -pub const FORMAT_VERSION: u32 = 3; +/// +/// Version 4: `FunctionMeta::Llm` removed the Borsh-serialized +/// `prompt_template` field. +pub const FORMAT_VERSION: u32 = 4; const MAGIC: [u8; 4] = *b"BEXC"; From becab69f45bd44ea3333729dc3db563296f7eb37 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 19:39:26 -0700 Subject: [PATCH 4/6] forward generic args to LLM spec companions --- .../crates/baml_compiler2_ast/src/companions.rs | 17 +++++++++++++++++ .../crates/baml_compiler2_ast/src/lower_cst.rs | 13 +++++++++++++ .../baml_compiler2_ast/src/lower_expr_body.rs | 9 ++++++--- .../crates/baml_compiler2_ppir/src/lib.rs | 14 ++++++++++++++ .../tests/convert_boundary_regressions.rs | 4 +++- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/baml_language/crates/baml_compiler2_ast/src/companions.rs b/baml_language/crates/baml_compiler2_ast/src/companions.rs index 20e141ad84b..13a1e97d29e 100644 --- a/baml_language/crates/baml_compiler2_ast/src/companions.rs +++ b/baml_language/crates/baml_compiler2_ast/src/companions.rs @@ -62,6 +62,22 @@ fn own_params(parent: &FunctionDef) -> Vec { .collect() } +fn generic_type_args(parent: &FunctionDef) -> Vec { + parent + .generic_params + .iter() + .map(|param| { + TypeExprKind::Path { + segments: vec![param.name.clone()], + generic_args: vec![], + associated_type_bindings: vec![], + attrs: vec![], + } + .at(parent.span) + }) + .collect() +} + fn companion_def( parent: &FunctionDef, name: Name, @@ -126,6 +142,7 @@ fn llm_render_prompt(parent: &FunctionDef) -> Option { let body = lower_expr_body::synthesize_spec_render_prompt_body( parent.name.as_str(), ¶m_names, + generic_type_args(parent), Some(out), parent.span, ); diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs index 047f8fb4f41..c06f9e896fb 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs @@ -397,9 +397,22 @@ fn lower_function( .iter() .any(|(t, _)| t == "spec") { + let spec_type_args = generic_params + .iter() + .map(|param| { + crate::ast::TypeExprKind::Path { + segments: vec![param.name.clone()], + generic_args: vec![], + associated_type_bindings: vec![], + attrs: vec![], + } + .at(llm_body_def.span) + }) + .collect(); let (expr_body, source_map) = lower_expr_body::synthesize_spec_agent_run_body( name.as_str(), ¶m_names, + spec_type_args, return_type.clone(), llm_body_def.span, ); diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs b/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs index 023d1f16834..e1cbeed505f 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs @@ -618,6 +618,7 @@ pub(crate) fn synthesize_llm_spec_body( pub(crate) fn synthesize_spec_render_prompt_body( function_name: &str, param_names: &[Name], + spec_type_args: Vec, out_type: Option, span: TextRange, ) -> (ExprBody, AstSourceMap) { @@ -636,7 +637,7 @@ pub(crate) fn synthesize_spec_render_prompt_body( let spec_call = ctx.alloc_expr( Expr::Call { callee: spec_callee, - type_args: vec![], + type_args: spec_type_args, args: spec_args, }, span, @@ -733,6 +734,7 @@ pub(crate) fn synthesize_spec_parse_body( pub(crate) fn synthesize_spec_agent_run_body( function_name: &str, param_names: &[Name], + spec_type_args: Vec, out_type: Option, span: TextRange, ) -> (ExprBody, AstSourceMap) { @@ -755,7 +757,7 @@ pub(crate) fn synthesize_spec_agent_run_body( let spec_call = ctx.alloc_expr( Expr::Call { callee: spec_callee, - type_args: vec![], + type_args: spec_type_args, args: spec_args, }, span, @@ -825,6 +827,7 @@ pub(crate) fn synthesize_spec_agent_run_body( pub fn synthesize_spec_stream_body( function_name: &str, param_names: &[Name], + spec_type_args: Vec, type_args: Vec, span: TextRange, ) -> (ExprBody, AstSourceMap) { @@ -847,7 +850,7 @@ pub fn synthesize_spec_stream_body( let spec_call = ctx.alloc_expr( Expr::Call { callee: spec_callee, - type_args: vec![], + type_args: spec_type_args, args: spec_args, }, span, diff --git a/baml_language/crates/baml_compiler2_ppir/src/lib.rs b/baml_language/crates/baml_compiler2_ppir/src/lib.rs index 19db9923af8..7229d04aa49 100644 --- a/baml_language/crates/baml_compiler2_ppir/src/lib.rs +++ b/baml_language/crates/baml_compiler2_ppir/src/lib.rs @@ -467,9 +467,23 @@ pub fn ppir_expansion_items(db: &dyn Db, file: SourceFile) -> PpirExpansionItems .filter(|p| p.name.as_str() != "client") .map(|p| p.name.clone()) .collect(); + let spec_type_args = func + .generic_params + .iter() + .map(|param| { + ast::TypeExprKind::Path { + segments: vec![param.name.clone()], + generic_args: vec![], + associated_type_bindings: vec![], + attrs: vec![], + } + .at(span) + }) + .collect(); let (body, source_map) = ast::synthesize_spec_stream_body( func.name.as_str(), ¶m_names, + spec_type_args, companion_type_args, span, ); diff --git a/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs b/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs index c99aded7a1d..a69edbfc2a0 100644 --- a/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs +++ b/baml_language/crates/baml_tests/tests/convert_boundary_regressions.rs @@ -69,8 +69,10 @@ fn throw_of_non_literal_expression_compiles() { fn generic_llm_function_with_generic_return_compiles() { let db = db_with( "class Box { value T }\n\ - function Extract(text: string) -> Box { client GPT4\nprompt `x` }\n", + client Dummy = openai.OpenAiClient.new(model = \"gpt-4\")\n\ + function Extract(text: string) -> Box { client Dummy\nprompt `x` }\n", ); + baml_project::testing::assert_no_diagnostic_errors(&db); assert!(bytecode_ok(&db).is_ok()); } From 80a4c3ddf85cf3fc32ef042f62c647734589673c Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 20:38:08 -0700 Subject: [PATCH 5/6] align Jinja migration diagnostic --- .../crates/baml_compiler2_ast/src/lower_cst.rs | 1 - .../src/lowering_diagnostic.rs | 18 +++++------------- ...__jinja_prompt_removed__05_diagnostics.snap | 4 ++-- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs index c06f9e896fb..f9280bc32ca 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs @@ -345,7 +345,6 @@ fn lower_function( // plain backtick templates through the spec. if let Some(raw_prompt) = llm.prompt_field().and_then(|pf| pf.raw_string()) { diags.push(LoweringDiagnostic::LlmJinjaPromptRemoved { - function_name: name.as_str().to_string(), span: raw_prompt.syntax().span_range(), }); } diff --git a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs index 6dce7b74280..30a74177e35 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs @@ -194,10 +194,7 @@ pub enum LoweringDiagnostic { /// A legacy Jinja `#"..."#` prompt on an LLM function. Removed: prompts /// are backtick templates. - LlmJinjaPromptRemoved { - function_name: String, - span: TextRange, - }, + LlmJinjaPromptRemoved { span: TextRange }, /// The LLM function's `client` value cannot be used: unknown provider /// prefix, a string without a `provider/model` shape, or the removed @@ -578,18 +575,13 @@ impl LoweringDiagnostic { *span, "retry composes at the client boundary now", ), - LoweringDiagnostic::LlmJinjaPromptRemoved { - function_name, - span, - } => ( + LoweringDiagnostic::LlmJinjaPromptRemoved { span } => ( DiagnosticId::InvalidSyntax, Severity::Error, - format!( - "LLM function `{function_name}` uses a Jinja `#\"...\"#` prompt, which is no \ - longer supported. Use a backtick prompt with `${{...}}` interpolation instead." - ), + "Jinja `#\"...\"#` prompts are no longer supported. Use a backtick prompt with `${...}` interpolation instead." + .to_string(), *span, - "replace `#\"Hello {{ name }}\"#` with `` `Hello ${name}` ``", + "use a backtick prompt instead", ), LoweringDiagnostic::InvalidLlmClient { function_name, diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap index a331635ca13..238d11bfd91 100644 --- a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/jinja_prompt_removed/baml_tests__diagnostic_errors__jinja_prompt_removed__05_diagnostics.snap @@ -4,9 +4,9 @@ source: crates/baml_tests/src/generated_tests.rs === COMPILER2 DIAGNOSTICS === [hir] E0010 - × LLM function `Greet` uses a Jinja `#"..."#` prompt, which is no longer supported. Use a backtick prompt with `${...}` interpolation instead. + × Jinja `#"..."#` prompts are no longer supported. Use a backtick prompt with `${...}` interpolation instead. ╭─[main.baml:3:12] 3 │ prompt #"Hello {{ name }}"# · ──────────┬───────── - · ╰── replace `#"Hello {{ name }}"#` with `` `Hello ${name}` `` + · ╰── use a backtick prompt instead ╰──── From d20b8641a724499b2cb8b0e679f0f53a69a60256 Mon Sep 17 00:00:00 2001 From: Avery Townsend Date: Tue, 11 Aug 2026 20:45:52 -0700 Subject: [PATCH 6/6] align template string migration diagnostic --- .../baml_compiler2_ast/src/lower_cst.rs | 3 +++ .../src/lowering_diagnostic.rs | 12 ++++++++++ .../crates/baml_compiler_parser/src/parser.rs | 22 +++---------------- .../template_string_removed/main.baml | 0 ...mplate_string_removed__05_diagnostics.snap | 12 ---------- ...ors__template_string_removed__03_ppir.snap | 4 ++++ ...rors__template_string_removed__04_tir.snap | 4 ++++ ...mplate_string_removed__05_diagnostics.snap | 12 ++++++++++ ...te_string_removed__10_formatter__main.snap | 4 ++++ 9 files changed, 42 insertions(+), 31 deletions(-) rename baml_language/crates/baml_tests/projects/{broken_syntax => diagnostic_errors}/template_string_removed/main.baml (100%) delete mode 100644 baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__03_ppir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__04_tir.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__05_diagnostics.snap create mode 100644 baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__10_formatter__main.snap diff --git a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs index f9280bc32ca..4bc41b33a6d 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lower_cst.rs @@ -156,6 +156,9 @@ pub fn lower_file_with_path_and_test_owner( diags.push(lower_generator_deprecation(&child)); } baml_compiler_syntax::SyntaxKind::TEMPLATE_STRING_DEF => { + diags.push(LoweringDiagnostic::TemplateStringRemoved { + span: child.span_range(), + }); if let Some(ts) = lower_template_string(&child, &mut diags) { items.push(Item::TemplateString(ts)); } diff --git a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs index 30a74177e35..f6074ecfb1e 100644 --- a/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs +++ b/baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs @@ -196,6 +196,10 @@ pub enum LoweringDiagnostic { /// are backtick templates. LlmJinjaPromptRemoved { span: TextRange }, + /// A legacy `template_string` declaration. Removed: use a function + /// returning a backtick string. + TemplateStringRemoved { span: TextRange }, + /// The LLM function's `client` value cannot be used: unknown provider /// prefix, a string without a `provider/model` shape, or the removed /// unquoted shorthand. @@ -583,6 +587,14 @@ impl LoweringDiagnostic { *span, "use a backtick prompt instead", ), + LoweringDiagnostic::TemplateStringRemoved { span } => ( + DiagnosticId::InvalidSyntax, + Severity::Error, + "`template_string` declarations are no longer supported. Use a function returning a backtick string instead." + .to_string(), + *span, + "use a function returning a backtick string instead", + ), LoweringDiagnostic::InvalidLlmClient { function_name, reason, diff --git a/baml_language/crates/baml_compiler_parser/src/parser.rs b/baml_language/crates/baml_compiler_parser/src/parser.rs index e20e7a6c741..1d61973d040 100644 --- a/baml_language/crates/baml_compiler_parser/src/parser.rs +++ b/baml_language/crates/baml_compiler_parser/src/parser.rs @@ -8017,13 +8017,6 @@ impl<'a> Parser<'a> { /// Parse a template string declaration pub(crate) fn parse_template_string(&mut self) { self.with_node(SyntaxKind::TEMPLATE_STRING_DEF, |p| { - p.error_here( - "`template_string` is no longer supported. Use a function returning a backtick string instead." - .to_string(), - ); - - // Keep parsing the declaration for lossless recovery and to avoid - // cascading diagnostics after the unsupported keyword. p.expect(TokenKind::TemplateString); // Template name @@ -9159,24 +9152,15 @@ function Demo() -> string {{ } #[test] - fn template_string_reports_targeted_unsupported_error() { + fn parses_template_string_for_lowering_diagnostic() { let source = "template_string Greeting(name: string) `Hello ${name}`"; let (root, errors) = parse_source(source); - assert_eq!(errors.len(), 1); - assert!( - matches!( - &errors[0], - ParseError::InvalidSyntax { message, .. } - if message.contains("`template_string` is no longer supported") - && message.contains("function returning a backtick string") - ), - "unexpected diagnostic: {errors:#?}" - ); + assert_no_errors(&errors); assert!( root.descendants() .any(|node| node.kind() == SyntaxKind::TEMPLATE_STRING_DEF), - "unsupported declaration should remain in the tree for recovery" + "unsupported declaration should remain in the tree for lowering" ); } diff --git a/baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml b/baml_language/crates/baml_tests/projects/diagnostic_errors/template_string_removed/main.baml similarity index 100% rename from baml_language/crates/baml_tests/projects/broken_syntax/template_string_removed/main.baml rename to baml_language/crates/baml_tests/projects/diagnostic_errors/template_string_removed/main.baml diff --git a/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap deleted file mode 100644 index 837244ea753..00000000000 --- a/baml_language/crates/baml_tests/snapshots/broken_syntax/template_string_removed/baml_tests__broken_syntax__template_string_removed__05_diagnostics.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/baml_tests/src/generated_tests.rs ---- -=== COMPILER2 DIAGNOSTICS === - [parse] E0010 - - × invalid syntax - ╭─[main.baml:1:1] - 1 │ template_string Greeting(name: string) `Hello ${name}` - · ───────┬─────── - · ╰── `template_string` is no longer supported. Use a function returning a backtick string instead. - ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__03_ppir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__03_ppir.snap new file mode 100644 index 00000000000..265d93b2c5b --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__03_ppir.snap @@ -0,0 +1,4 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== PPIR === diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__04_tir.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__04_tir.snap new file mode 100644 index 00000000000..da37c0436f4 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__04_tir.snap @@ -0,0 +1,4 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== TIR2 === diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__05_diagnostics.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__05_diagnostics.snap new file mode 100644 index 00000000000..3c5aaa45b65 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__05_diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +=== COMPILER2 DIAGNOSTICS === + [hir] E0010 + + × `template_string` declarations are no longer supported. Use a function returning a backtick string instead. + ╭─[main.baml:1:1] + 1 │ template_string Greeting(name: string) `Hello ${name}` + · ───────────────────────────┬────────────────────────── + · ╰── use a function returning a backtick string instead + ╰──── diff --git a/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__10_formatter__main.snap b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__10_formatter__main.snap new file mode 100644 index 00000000000..7e1c21bbd98 --- /dev/null +++ b/baml_language/crates/baml_tests/snapshots/diagnostic_errors/template_string_removed/baml_tests__diagnostic_errors__template_string_removed__10_formatter__main.snap @@ -0,0 +1,4 @@ +--- +source: crates/baml_tests/src/generated_tests.rs +--- +template_string Greeting(name: string) `Hello ${name}`