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

Filter by extension

Filter by extension


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

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

2 changes: 2 additions & 0 deletions baml_language/crates/baml_compiler2_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,8 @@ pub struct ConfigItemDef {
pub struct TestDef {
pub name: Name,
pub config_items: Vec<ConfigItemDef>,
/// Pre-serialized JSON object built from the `args { … }` block in the CST.
pub args_json: String,
pub span: TextRange,
pub name_span: TextRange,
}
Expand Down
139 changes: 139 additions & 0 deletions baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,9 +727,22 @@ fn lower_test(node: &SyntaxNode, diags: &mut Vec<LoweringDiagnostic>) -> Option<
.map(|cb| lower_config_block(&cb, "test", &test_name, diags))
.unwrap_or_default();

// Build JSON from the CST `args { … }` block before structural info is lost.
let args_json = test
.config_block()
.and_then(|cb| {
cb.items().find(|item| {
item.key().map(|k| k.text() == "args").unwrap_or(false)
})
})
.and_then(|item| item.nested_block())
.map(|block| cst_config_block_to_json(&block))
.unwrap_or_else(|| "{}".to_string());

Some(TestDef {
name: Name::new(&test_name),
config_items,
args_json,
span: node.text_range(),
name_span: name_token.text_range(),
})
Expand Down Expand Up @@ -1543,6 +1556,132 @@ fn validate_client_options(
}
}

/// Convert a CST `ConfigBlock` (the `args { … }` block of a test) into a JSON object string.
fn cst_config_block_to_json(cb: &ast::ConfigBlock) -> String {
let entries: Vec<String> = cb
.items()
.filter_map(|item| {
let key = item.key()?.text().to_string();
let value = cst_config_item_value_to_json(&item);
Some(format!("\"{}\":{}", escape_json_string(&key), value))
})
.collect();
format!("{{{}}}", entries.join(","))
}

/// Convert a single CST config item's value to a JSON value string.
fn cst_config_item_value_to_json(item: &ast::ConfigItem) -> String {
use baml_compiler_syntax::SyntaxKind;

// Nested block → JSON object
if let Some(nested) = item.nested_block() {
return cst_config_block_to_json(&nested);
}

// Array literal → JSON array
if item.is_array() {
if let Some(arr_node) = item.array_node() {
let elements: Vec<String> = arr_node
.children()
.filter(|child| child.kind() == SyntaxKind::CONFIG_VALUE)
.map(|cv: baml_compiler_syntax::SyntaxNode| cst_config_value_node_to_json(&cv))
.collect();
return format!("[{}]", elements.join(","));
}
return "[]".to_string();
}

// Scalar value
if let Some(cv_node) = item.config_value_node() {
return cst_config_value_node_to_json(&cv_node);
}

"null".to_string()
}

/// Convert a CONFIG_VALUE syntax node to a JSON value string.
fn cst_config_value_node_to_json(cv_node: &baml_compiler_syntax::SyntaxNode) -> String {
use baml_compiler_syntax::SyntaxKind;

// Collect all meaningful tokens
let tokens: Vec<_> = cv_node
.descendants_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.filter(|t| !t.kind().is_trivia())
.collect();

if tokens.is_empty() {
return "null".to_string();
}

// Check for string literal (has QUOTE tokens)
let has_quotes = tokens.iter().any(|t| t.kind() == SyntaxKind::QUOTE);
if has_quotes {
// Extract the string content (everything between quotes)
let text: String = tokens
.iter()
.filter(|t| t.kind() != SyntaxKind::QUOTE)
.map(|t| t.text().to_string())
.collect();
return format!("\"{}\"", escape_json_string(&text));
}

// Check for array literal
if cv_node
.children()
.any(|child| child.kind() == SyntaxKind::ARRAY_LITERAL)
{
if let Some(arr_node) = cv_node
.children()
.find(|child| child.kind() == SyntaxKind::ARRAY_LITERAL)
{
let elements: Vec<String> = arr_node
.children()
.filter(|child| child.kind() == SyntaxKind::CONFIG_VALUE)
.map(|cv| cst_config_value_node_to_json(&cv))
.collect();
return format!("[{}]", elements.join(","));
}
}

// Check for integer literal
let all_int = tokens
.iter()
.all(|t| matches!(t.kind(), SyntaxKind::INTEGER_LITERAL | SyntaxKind::MINUS));
if all_int {
let text: String = tokens.iter().map(|t| t.text().to_string()).collect();
return text;
}

// Check for float literal
let all_float = tokens.iter().all(|t| {
matches!(
t.kind(),
SyntaxKind::FLOAT_LITERAL | SyntaxKind::MINUS
)
});
if all_float {
let text: String = tokens.iter().map(|t| t.text().to_string()).collect();
return text;
}

// Bare word — check for booleans and null
let text: String = tokens.iter().map(|t| t.text().to_string()).collect();
match text.as_str() {
"true" | "false" | "null" => text,
// Bare word treated as string
_ => format!("\"{}\"", escape_json_string(&text)),
}
}

fn escape_json_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}

fn lower_config_block(
cb: &ast::ConfigBlock,
block_kind: &'static str,
Expand Down
7 changes: 5 additions & 2 deletions baml_language/crates/baml_compiler2_hir/src/item_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ pub struct Test {
pub function_refs: Vec<Name>,
/// Test arguments as key-value pairs.
pub args: Vec<(Name, TestArgValue)>,
/// Pre-serialized JSON of args, built from the CST before structural info is lost.
pub raw_args_json: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -462,18 +464,19 @@ impl ItemTree {
// Values may be comma-separated or a single name
item.value
.split(',')
.map(|s| Name::new(s.trim().trim_matches('"')))
.map(|s| Name::new(s.trim().trim_matches(|c| c == '"' || c == '[' || c == ']').trim()))
.collect::<Vec<_>>()
})
.collect();
// Args come from config_items with key "args" — store raw; complex parsing skipped
// Structured args parsing skipped for now; use pre-serialized JSON from CST lowering.
let args = Vec::new();
self.tests.insert(
id,
Test {
name: t.name.clone(),
function_refs,
args,
raw_args_json: t.args_json.clone(),
},
);
id
Expand Down
3 changes: 1 addition & 2 deletions baml_language/crates/baml_project/src/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ pub fn list_tests_with_metadata(db: &ProjectDatabase) -> Vec<TestSymbol> {
.map(std::string::ToString::to_string)
.unwrap_or_default();

// args parsing is skipped in canary's alloc_test — always empty for now
let args_json = "{}".to_string();
let args_json = test.raw_args_json.clone();

result.push(TestSymbol {
name: name.to_string(),
Expand Down
2 changes: 2 additions & 0 deletions baml_language/crates/bex_project/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ workspace = true
baml_base = { workspace = true }
baml_builtins = { workspace = true }
baml_compiler2_emit = { workspace = true }
baml_compiler2_hir = { workspace = true }
baml_compiler2_tir = { workspace = true }
baml_compiler2_visualization = { workspace = true }
baml_compiler_diagnostics = { workspace = true }
baml_db = { workspace = true }
Expand Down
36 changes: 36 additions & 0 deletions baml_language/crates/bex_project/src/bex_lsp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,42 @@ pub struct FunctionInfo {
pub kind: FunctionKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<LlmCapabilities>,
pub params: Vec<ParamInfo>,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ParamInfo {
pub name: String,
pub field_type: FieldType,
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum FieldType {
String,
Int,
Float,
Bool,
Null,
Enum { name: String, values: Vec<String> },
Class { name: String, fields: Vec<ParamInfo> },
List { item: Box<FieldType> },
Map { key: Box<FieldType>, value: Box<FieldType> },
Optional { inner: Box<FieldType> },
Union { variants: Vec<FieldType> },
Literal { value: LiteralValue },
RecursiveRef { name: String },
Any,
Media { media_type: String },
}

#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum LiteralValue {
String(String),
Int(i64),
Bool(bool),
}

#[derive(Debug, Clone, serde::Serialize)]
Expand Down
Loading
Loading