Simplified BAML Runtime
A minimal, educational implementation of the BAML (Basically A Markup Language) runtime that demonstrates the core concepts by reducing the original ~50K line codebase to approximately ~5K lines.
✨ New Features:
- Automatic IR generation from native Rust types using
#[derive(BamlSchema)]macros! - Function definitions with
#[baml_function]attribute macro - Client configuration with
#[derive(BamlClient)]derive macro - All macros use consistent attribute syntax for a unified API
What is BAML?
BAML is a language for defining and calling LLM functions with structured outputs. The runtime handles:
- Converting type definitions into human-readable schemas
- Injecting schemas into Jinja2 templates
- Calling LLM APIs
- Parsing and validating LLM responses
Core Components
This simplified implementation consists of 7 key components:
1. IR (Intermediate Representation) - src/ir.rs
Defines the core types:
Class- Structured types with fieldsEnum- Enumerated typesFunction- LLM functionsBamlValue- Runtime valuesBamlSchematrait - For automatic IR generation
2. Macro System - simplify_baml_macros/
Procedural macros for automatic IR generation with consistent syntax:
#[derive(BamlSchema)]- Automatically implement BamlSchema trait for structs and enums#[derive(BamlClient)]- Configure LLM clients (OpenAI, Anthropic, custom)#[baml_function(client = "...")]- Define BAML functions with type-safe syntax#[baml(description = "...")]- Add descriptions to types, fields, and parameters#[baml(rename = "...")]- Rename fields in the generated schema
3. Schema Registry - src/registry.rs
Collects types and builds IR:
BamlSchemaRegistry::new()- Create registry.register::<T>()- Register types implementing BamlSchema.build()- Generate final IR
4. Schema Formatter - src/schema.rs
Converts IR types into human-readable schema strings:
Month
----
- January
- February
- March
Answer in JSON using this schema:
{
name: string,
age: int,
birthMonth: Month,
}
5. Template Renderer - src/renderer.rs
Uses Jinja2 (via minijinja) to render prompts with automatic schema injection.
6. HTTP Client - src/client.rs
Simple wrapper for calling LLM APIs (OpenAI, Anthropic, or custom endpoints).
7. Parser - src/parser.rs
Lenient JSON parser with type coercion that handles:
- Markdown code blocks
- Type conversions (string to int, etc.)
- Enum validation
- Nested structures
8. Runtime - src/runtime.rs
Orchestrates all components to execute BAML functions.
Quick Start
Installation
Add to your Cargo.toml:
[]
= { = "/path/to/simplify_baml" }
= { = "1.0", = ["full"] }
Basic Usage (With Macros - Recommended!)
use *;
use ;
use HashMap;
// 1. Define types using derive macros - clean and type-safe!
// 2. Define BAML function using macro
// 3. Configure client using derive macro
;
async
If you prefer to build IR manually without macros:
use *;
use HashMap;
async
Running Examples
# Set your OpenAI API key
# Run the complete macro example (recommended - shows all 3 macros!)
# Run the class/enum macro example
# Run the manual IR building example
# Run nested structures with macros
Running Tests
Macro System Features
BAML provides three powerful macros that dramatically simplify development:
1. #[derive(BamlSchema)] - Type Definitions
The derive macro makes IR generation dramatically simpler and more maintainable:
Type Mapping
String→FieldType::Stringi64,i32,i16,i8→FieldType::Intf64,f32→FieldType::Floatbool→FieldType::BoolOption<T>→ Makes field optionalVec<T>→FieldType::List(T)- Custom types → Automatically detected as
ClassorEnum
Attributes
#[baml(description = "...")]- Add descriptions to types and fields#[baml(rename = "field_name")]- Rename fields in generated schema
Example: Complex Nested Structures
use BamlSchema;
// Automatic IR generation - handles all nesting!
let ir = new
.
.
.
.
.build;
Benefits:
- ✅ ~55% less code compared to manual IR building
- ✅ Type-safe - catches errors at compile time
- ✅ More readable and maintainable
- ✅ Automatic handling of nested structures
- ✅ Field renaming and descriptions
2. #[baml_function] - Function Definitions
Define BAML functions using natural Rust syntax instead of verbose struct construction:
// Use it in your IR
let ir = new
.
.build_with_functions;
Key Features:
- ✅ Function name automatically converted to PascalCase (extract_person → ExtractPerson)
- ✅ Type-safe input/output definitions
- ✅ Parameter descriptions via
#[baml(description)] - ✅ Jinja2 template as function body
- ✅ Generates a function that returns
Functionstruct
3. #[derive(BamlClient)] - Client Configuration
Configure LLM clients using a derive macro - consistent with the BamlSchema pattern:
use BamlClient;
// OpenAI client
;
// Anthropic client
;
// Custom endpoint
;
// Use it
let client = new;
Benefits:
- ✅ Consistent syntax with
#[derive(BamlSchema)] - ✅ Supports OpenAI, Anthropic, and custom endpoints
- ✅ Type-safe API
- ✅ Generates a
new(api_key: String) -> LLMClientmethod
What's Different from Full BAML?
This simplified version focuses on the core execution path and omits:
- ❌ BAML language parser (use Rust macros instead)
- ❌ CLI tools
- ❌ WASM support
- ❌ Advanced tracing/telemetry
- ❌ Test runner
- ❌ Code generation for multiple languages
- ❌ VS Code extension integration
- ❌ Streaming support
- ❌ Complex retry policies and orchestration strategies
- ❌ Multiple prompt configs per function
What it keeps (and improves!):
- ✅ Core IR types (Class, Enum, Function)
- ✅ Automatic IR generation from Rust types via
#[derive(BamlSchema)]🆕 - ✅ Function definitions via
#[baml_function]🆕 - ✅ Client configuration via
#[derive(BamlClient)]🆕 - ✅ Consistent attribute-based syntax across all macros 🆕
- ✅ Schema formatting (types → human-readable strings)
- ✅ Jinja2 template rendering
- ✅ HTTP client for LLM calls
- ✅ Lenient JSON parsing with type coercion
- ✅ Basic runtime orchestration
Key Insights
How BAML Actually Works
-
IR as a Bidirectional Contract: The Intermediate Representation (IR) is the single source of truth that serves dual purposes:
- Outbound (Generation): IR → SchemaFormatter converts types into human-readable schemas that tell the LLM what structure to return
- Inbound (Parsing): IR → Parser validates and coerces the LLM's response back into typed values
This bidirectional design ensures type safety and consistency - the same type definitions generate both the prompt instructions AND validate the results:
┌─────────────────────────────────────────────────────┐ │ IR (Single Source of Truth) │ │ - Classes, Enums, Functions │ │ - Field types and structure │ └──────────────┬──────────────────────┬────────────────┘ │ │ │ (Generate) │ (Parse) ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Schema Formatter │ │ Parser │ │ (src/schema.rs) │ │ (src/parser.rs) │ └────────┬─────────┘ └────────▲─────────┘ │ │ ▼ │ Human-readable JSON from schema text LLM response │ │ └──> Prompt to LLM ────┘ -
Schema Auto-Generation: BAML automatically converts your type definitions into human-readable schemas and appends them to prompts.
-
Two-Stage Parsing:
- Stage 1: Lenient JSON extraction (handles markdown, extra text)
- Stage 2: Type coercion (converts values to match expected types)
-
Jinja2 Templates: User prompts are Jinja2 templates with automatic
output_schemavariable injection. -
Simple Flow:
IR → Schema Formatter → Jinja2 → HTTP Client → Lenient Parser → Typed Result
Transformation Pipeline Deep Dive
Understanding the complete transformation from Rust struct to LLM prompt and back is key to understanding BAML. Here's the full pipeline with concrete examples:
Step 1: Rust Struct Definition
// Define your types using Rust syntax with derive macros
Location: User code (e.g., examples/with_macros.rs:7-25)
Step 2: IR (Intermediate Representation)
The #[derive(BamlSchema)] macro automatically generates code that produces this IR:
// Generated IR representation
Class
Enum
Location: src/ir.rs:7-60 (IR types), simplify_baml_macros/src/lib.rs:74-127 (macro generation)
Key Transformations:
String→FieldType::Stringi64→FieldType::IntOption<T>→optional: trueVec<T>→FieldType::List(T)#[baml(rename = "...")]→ Changes field name in schema- Enum variants → List of string values
Step 3: Human-Readable Schema
The SchemaFormatter converts IR to a human-readable format:
Month
----
- January
- February
- March
- April
- May
- June
- July
- August
- September
- October
- November
- December
Answer in JSON using this schema:
{
name: string,
age: int,
birthMonth: Month,
occupation: string,
}
Location: src/schema.rs:10-140
Process:
- Collect all dependencies (enums and nested classes)
- Render enum definitions with list of values
- Render the main schema in pseudo-JSON format
- Add instruction: "Answer in JSON using this schema:"
Step 4: Final Prompt (Template + Schema)
The PromptRenderer combines your Jinja2 template with the generated schema:
Extract the person's information from the following text:
John Smith is 30 years old and was born in March. He works as a software engineer.
Please extract: name, age, birth month (if mentioned), and occupation (if mentioned).
Month
----
- January
- February
- March
- April
- May
- June
- July
- August
- September
- October
- November
- December
Answer in JSON using this schema:
{
name: string,
age: int,
birthMonth: Month,
occupation: string,
}
Location: src/renderer.rs:9-70
Process:
- Generate schema from IR (via
SchemaFormatter) - Create Jinja2 environment (using
minijinja) - Render template with user parameters
- Append schema if not already in template (automatic injection!)
Step 5: LLM Response
The prompt is sent to the LLM via HTTP, and it returns a response:
Here's the extracted information:
```json
{
"name": "John Smith",
"age": "30",
"birthMonth": "march",
"occupation": "software engineer"
}
**Location**: `src/client.rs:10-80` (HTTP client)
**Note**: The LLM may return:
- Extra text before/after JSON
- Markdown code blocks
- Incorrect types (e.g., `"30"` as string instead of int)
- Different casing (e.g., `"march"` instead of `"March"`)
### **Step 6: Parsed and Coerced Result**
The `Parser` extracts JSON and coerces types to match the schema:
```rust
BamlValue::Map({
"name": BamlValue::String("John Smith"),
"age": BamlValue::Int(30), // "30" → 30 (string to int coercion)
"birthMonth": BamlValue::String("March"), // "march" → "March" (enum normalization)
"occupation": BamlValue::String("software engineer"),
})
Location: src/parser.rs:15-200
Process:
- Extract JSON: Find JSON in response (handles markdown blocks, extra text)
- Look for
```jsonblocks - Look for plain
```blocks - Find
{ ... }boundaries
- Look for
- Parse JSON: Use
serde_jsonto parse string - Type Coercion: Convert values to match target types
- String to Int: Parse
"30"→30 - String to Float: Parse
"3.14"→3.14 - Enum: Case-insensitive matching (
"march"→"March") - Nested structures: Recursively validate and coerce
- String to Int: Parse
Complete Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ 1. RUST STRUCT │
│ #[derive(BamlSchema)] │
│ struct Person { name: String, age: i64, ... } │
└────────────────────┬────────────────────────────────────────┘
│
│ Procedural Macro
│ (simplify_baml_macros/src/lib.rs)
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. IR (Intermediate Representation) │
│ Class { name: "Person", fields: [...] } │
└────────────────────┬────────────────────────────────────────┘
│
│ SchemaFormatter::render()
│ (src/schema.rs)
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. HUMAN-READABLE SCHEMA │
│ Answer in JSON using this schema: │
│ { name: string, age: int, birthMonth: Month, ... } │
└────────────────────┬────────────────────────────────────────┘
│
│ PromptRenderer::render()
│ (src/renderer.rs + minijinja)
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. FINAL PROMPT │
│ Extract info from: {{ text }} │
│ [schema appended automatically] │
└────────────────────┬────────────────────────────────────────┘
│
│ LLMClient::call()
│ (src/client.rs - HTTP request)
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. LLM RESPONSE │
│ ```json │
│ { "name": "John", "age": "30", ... } │
│ ``` │
└────────────────────┬────────────────────────────────────────┘
│
│ Parser::parse()
│ (src/parser.rs - extract + coerce)
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. TYPED BAMLVALUE │
│ BamlValue::Map({ │
│ "name": String("John"), │
│ "age": Int(30), // Coerced from "30" │
│ }) │
└─────────────────────────────────────────────────────────────┘
Runtime Orchestration
All of this is orchestrated by BamlRuntime::execute():
// src/runtime.rs:18-48
pub async
Key Design Decisions
-
Lenient Parsing: The parser is intentionally lenient, handling markdown, type mismatches, and extra text. This makes it work reliably with real LLM outputs.
-
Schema Injection: Schemas are automatically appended to prompts, ensuring the LLM always knows the expected structure.
-
Type Coercion: Automatic conversion between compatible types (string ↔ number, case normalization for enums) reduces friction.
-
Compile-Time Safety: Using Rust macros instead of a custom DSL provides type safety at compile time.
-
Minimal Dependencies: The entire pipeline uses only essential dependencies:
minijinjafor templates,reqwestfor HTTP,serde_jsonfor JSON parsing.
Architecture Comparison
Original BAML (~50K lines)
- Full language parser and compiler
- Multi-language code generation
- Complex orchestration strategies
- Extensive tracing and telemetry
- WASM compilation
- CLI tools and VS Code integration
Simplified BAML (~6K lines)
- Rust macro-based IR generation 🆕
#[derive(BamlSchema)]for types#[baml_function]for functions#[derive(BamlClient)]for clients- Consistent attribute-based syntax
- Single language (Rust)
- Basic orchestration
- Minimal logging
- Native only
- Library-only interface
Innovation: While the original BAML requires writing schemas in a custom DSL, Simplified BAML uses Rust's macro system to generate IR directly from native code. This provides:
- ✨ Compile-time type safety
- ✨ No separate DSL parser needed
- ✨ Natural Rust syntax for all definitions
- ✨ Consistent API across all macros
- ✨ ~60% less code overall
License
This is an educational implementation demonstrating the core concepts of BAML. For production use, please use the official BAML runtime.
Learn More
- Original BAML: https://github.com/BoundaryML/baml
- BAML Documentation: https://docs.boundaryml.com