Skip to content
Draft
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.

100 changes: 90 additions & 10 deletions baml_language/crates/baml_base/src/files.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,109 @@
//! File management with Salsa 2022 API.
//!
//! Defines the core structures for accessing file contents and paths.
//!
//! ## Virtual file classification
//!
//! The compiler uses two categories of non-user files, identified by path prefix:
//!
//! | Category | Prefix | Example |
//! |-----------|-----------------|--------------------------------------|
//! | Built-in | `<builtin>/` | `<builtin>/baml/llm.baml` |
//! | Generated | `<generated:` | `<generated:stream/resume.baml>` |
//!
//! The canonical prefix constants live in this file.
//! Other crates should classify files through the [`SourceFile::is_builtin`],
//! [`SourceFile::is_generated`], and [`SourceFile::is_virtual`] methods, and
//! only use [`SourceFile::builtin_path_prefix`] /
//! [`SourceFile::generated_path_prefix`] when they must construct or strip
//! virtual paths.

use std::path::PathBuf;

use crate::FileId;

/// Input structure representing a source file in the compilation.
// ── Canonical virtual-file path prefixes ──────────────────────────────────────
const BUILTIN_PATH_PREFIX: &str = "<builtin>/";
const GENERATED_PATH_PREFIX: &str = "<generated:";

/// A source file tracked by the Salsa database.
///
/// This is a salsa input, which means it's the primary way to provide
/// source text to the compiler. The struct itself just stores an ID,
/// with the actual data stored in the salsa database.
/// This is a salsa input, which means it is the primary way to provide source
/// text to the compiler. The struct itself is a lightweight handle; the actual
/// data lives in the Salsa database and is accessed through the generated
/// field accessors (`text`, `path`, `file_id`).
#[salsa::input]
pub struct SourceFile {
/// Source text for the file
/// The full source text of the file.
#[returns(ref)]
pub text: String,

/// File path (for diagnostics and error reporting)
/// File path used for diagnostics and error reporting.
///
/// For real files this is an absolute filesystem path.
/// For virtual files it is one of the synthetic prefixed paths described
/// in the module-level documentation.
pub path: PathBuf,

/// The FileId associated with this source file.
/// The [`FileId`] associated with this source file.
///
/// Used to create lightweight Span values that can be embedded in tokens.
/// This allows spans to identify their source file without carrying
/// the full SourceFile reference (which is a Salsa-tracked entity).
/// Stored here so that lightweight [`crate::Span`] values can identify
/// their origin file without carrying the full `SourceFile` handle.
pub file_id: FileId,
}

impl SourceFile {
// ── Classification predicates ────────────────────────────────────────────

/// Returns `true` if this file is a compiler-provided built-in.
///
/// Built-in files (path prefix `<builtin>/`) define internal standard-library
/// types and functions. They are excluded from user-facing validation and
/// diagnostic reporting.
pub fn is_builtin(self, db: &dyn salsa::Database) -> bool {
self.has_prefix(db, BUILTIN_PATH_PREFIX)
}

/// Returns `true` if this file was synthesized by a compiler pass.
///
/// Generated files (path prefix `<generated:`) are internal implementation
/// details — currently only PPIR stream expansions — and must not be
/// surfaced to users as source locations.
pub fn is_generated(self, db: &dyn salsa::Database) -> bool {
self.has_prefix(db, GENERATED_PATH_PREFIX)
}

/// Returns `true` if this file is virtual, i.e. either built-in or generated.
///
/// Virtual files are not user-authored and should be skipped by passes that
/// operate only on user source (e.g. name validation, stream diagnostics).
pub fn is_virtual(self, db: &dyn salsa::Database) -> bool {
self.has_prefix(db, BUILTIN_PATH_PREFIX) || self.has_prefix(db, GENERATED_PATH_PREFIX)
}

// ── Prefix accessors ────────────────────────────────────────────────────

/// Returns the path prefix string used for built-in files (`"<builtin>/"`).
///
/// Use this only when you need to **construct** or **strip** a built-in
/// path (e.g. in `baml_builtins` or `baml_compiler_hir::file_namespace`).
/// For classification, prefer [`is_builtin`](Self::is_builtin).
pub fn builtin_path_prefix() -> &'static str {
BUILTIN_PATH_PREFIX
}

/// Returns the path prefix string used for generated files (`"<generated:"`).
///
/// Use this only when you need to **construct** a generated path (e.g. in
/// `baml_compiler_ppir::ppir_expansion_cst`).
/// For classification, prefer [`is_generated`](Self::is_generated).
pub fn generated_path_prefix() -> &'static str {
GENERATED_PATH_PREFIX
}

// ── private ──────────────────────────────────────────────────────────────

fn has_prefix(self, db: &dyn salsa::Database, prefix: &str) -> bool {
self.path(db).to_string_lossy().starts_with(prefix)
}
}
14 changes: 9 additions & 5 deletions baml_language/crates/baml_builtins/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,10 @@ mod tests {
// ============================================================================

/// Builtin BAML source files for built-in functions.
pub const BUILTIN_PATH_PREFIX: &str = "<builtin>/";
/// The path prefix for builtin files, sourced from `baml_base::SourceFile`.
pub fn builtin_path_prefix() -> &'static str {
baml_base::SourceFile::builtin_path_prefix()
}
///
/// These files are compiled together with user code and provide
/// implementations for builtin namespaces like `baml.llm`.
Expand Down Expand Up @@ -903,16 +906,17 @@ pub fn baml_sources() -> impl Iterator<Item = &'static baml_sources::BuiltinSour

#[cfg(test)]
mod builtin_path_tests {
use super::{BUILTIN_PATH_PREFIX, baml_sources};
use super::baml_sources;

#[test]
fn test_builtin_path_prefix_consistent_with_all() {
let prefix = baml_base::SourceFile::builtin_path_prefix();
for source in baml_sources::ALL {
assert!(
source.path.starts_with(BUILTIN_PATH_PREFIX),
"BuiltinSource path {:?} does not start with BUILTIN_PATH_PREFIX ({:?})",
source.path.starts_with(prefix),
"BuiltinSource path {:?} does not start with builtin_path_prefix ({:?})",
source.path,
BUILTIN_PATH_PREFIX,
prefix,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub fn file_package(db: &dyn crate::Db, file: SourceFile) -> PackageInfo {
let path = file.path(db);
let path_str = path.to_string_lossy();

if let Some(relative) = path_str.strip_prefix("<builtin>/") {
if let Some(relative) = path_str.strip_prefix(baml_base::SourceFile::builtin_path_prefix()) {
// <builtin>/baml/llm.baml → package "baml", namespace ["llm"]
// <builtin>/env.baml → package "env", namespace []
let segments: Vec<&str> = relative.split('/').collect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const DUPLICATE_VARIANT: ErrorCode = ErrorCode(13);
const DUPLICATE_ATTRIBUTE: ErrorCode = ErrorCode(14);
const UNKNOWN_ATTRIBUTE: ErrorCode = ErrorCode(15);
const INVALID_ATTRIBUTE_CONTEXT: ErrorCode = ErrorCode(16);
const CONFLICTING_STREAM_ATTRIBUTES: ErrorCode = ErrorCode(101);

// Generator diagnostics
const UNKNOWN_GENERATOR_PROPERTY: ErrorCode = ErrorCode(17);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use ariadne::{Label, ReportBuilder};
use baml_base::Span;

use super::{
ARGUMENT_COUNT_MISMATCH, CompilerError, DUPLICATE_ATTRIBUTE, DUPLICATE_FIELD, DUPLICATE_NAME,
DUPLICATE_VARIANT, ErrorCode, FIELD_NAME_MATCHES_TYPE_NAME, HTTP_CONFIG_NOT_BLOCK,
HirDiagnostic, INVALID_ATTRIBUTE_CONTEXT, INVALID_CLIENT_RESPONSE_TYPE,
ARGUMENT_COUNT_MISMATCH, CONFLICTING_STREAM_ATTRIBUTES, CompilerError, DUPLICATE_ATTRIBUTE,
DUPLICATE_FIELD, DUPLICATE_NAME, DUPLICATE_VARIANT, ErrorCode, FIELD_NAME_MATCHES_TYPE_NAME,
HTTP_CONFIG_NOT_BLOCK, HirDiagnostic, INVALID_ATTRIBUTE_CONTEXT, INVALID_CLIENT_RESPONSE_TYPE,
INVALID_CONSTRAINT_SYNTAX, INVALID_GENERATOR_PROPERTY_VALUE, INVALID_OPERATOR,
MISSING_CONDITION_PARENS, MISSING_GENERATOR_PROPERTY, MISSING_PROVIDER,
MISSING_RETURN_EXPRESSION, MISSING_SEMICOLON, NEGATIVE_TIMEOUT, NO_SUCH_FIELD,
Expand Down Expand Up @@ -334,6 +334,20 @@ where
span,
INVALID_ATTRIBUTE_CONTEXT,
),
HirDiagnostic::ConflictingStreamAttributes {
first_attr,
second_attr,
first_span,
second_span,
} => (
Report::build(ReportKind::Error, second_span)
.with_message(format!(
"Conflicting stream attributes '@{first_attr}' and '@{second_attr}'"
))
.with_label(Label::new(second_span).with_message("conflicting attribute"))
.with_label(Label::new(first_span).with_message("first attribute here")),
CONFLICTING_STREAM_ATTRIBUTES,
),
HirDiagnostic::UnknownGeneratorProperty {
generator_name,
property_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ pub enum DiagnosticId {
// Constraint attribute errors (E0032)
InvalidConstraintSyntax,

// Attribute value errors (E0037-E0038)
// Attribute value errors (E0037-E0038, E0101)
InvalidAttributeArg,
UnexpectedAttributeArg,
ConflictingStreamAttributes,

// Type literal errors (E0033)
UnsupportedFloatLiteral,
Expand Down Expand Up @@ -254,6 +255,7 @@ impl DiagnosticId {
// Attribute value errors
DiagnosticId::InvalidAttributeArg => "E0037",
DiagnosticId::UnexpectedAttributeArg => "E0038",
DiagnosticId::ConflictingStreamAttributes => "E0101",

// Type literal errors
DiagnosticId::UnsupportedFloatLiteral => "E0033",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub enum HirDiagnostic {
UnknownAttribute {
attr_name: String,
span: Span,
valid_attributes: Vec<&'static str>,
valid_attributes: &'static [&'static str],
},

/// Attribute used in wrong context.
Expand Down Expand Up @@ -245,6 +245,14 @@ pub enum HirDiagnostic {
/// Attribute takes no arguments but received some (e.g., @@dynamic("unexpected")).
UnexpectedAttributeArg { attr_name: String, span: Span },

/// Conflicting stream attributes on the same type expression.
ConflictingStreamAttributes {
first_attr: &'static str,
second_attr: &'static str,
first_span: Span,
second_span: Span,
},

// ============ Type Diagnostics ============
/// Float literal used as a type, which is not supported.
UnsupportedFloatLiteral { value: String, span: Span },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,20 @@ impl ToDiagnostic for HirDiagnostic {
)
.with_primary(*span, "unexpected argument"),

HirDiagnostic::ConflictingStreamAttributes {
first_attr,
second_attr,
first_span,
second_span,
} => Diagnostic::error(
DiagnosticId::ConflictingStreamAttributes,
format!(
"Conflicting stream attributes `@{first_attr}` and `@{second_attr}`."
),
)
.with_primary(*second_span, "conflicting attribute")
.with_secondary(*first_span, "first attribute here"),

HirDiagnostic::UnsupportedFloatLiteral { value, span } => Diagnostic::error(
DiagnosticId::UnsupportedFloatLiteral,
format!("Float literal values are not supported: {value}"),
Expand Down
11 changes: 3 additions & 8 deletions baml_language/crates/baml_compiler_hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,6 @@ pub fn function_signature_source_map<'db>(
source_map
}

/// The prefix used for builtin BAML files.
///
/// Files with paths starting with this prefix are treated as builtins
/// and their functions are namespaced accordingly.
pub const BUILTIN_PATH_PREFIX: &str = "<builtin>/";

/// Derive the namespace for a file based on its path.
///
/// Builtin files (paths starting with `<builtin>/`) get namespaced:
Expand All @@ -316,12 +310,13 @@ pub fn file_namespace(db: &dyn Db, file: SourceFile) -> Option<Namespace> {
let path = file.path(db);
let path_str = path.to_string_lossy();

if !path_str.starts_with(BUILTIN_PATH_PREFIX) {
if !file.is_builtin(db) {
return None;
}

// Extract path after prefix: "<builtin>/baml/llm.baml" -> "baml/llm.baml"
let after_prefix = &path_str[BUILTIN_PATH_PREFIX.len()..];
let prefix_len = SourceFile::builtin_path_prefix().len();
let after_prefix = &path_str[prefix_len..];

// Remove .baml extension and split by /
let without_ext = after_prefix.strip_suffix(".baml").unwrap_or(after_prefix);
Expand Down
1 change: 1 addition & 0 deletions baml_language/crates/baml_compiler_ppir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ workspace = true

[dependencies]
baml_base = { workspace = true }
baml_compiler_diagnostics = { workspace = true }
baml_compiler_parser = { workspace = true }
baml_compiler_syntax = { workspace = true }
baml_workspace = { workspace = true }
Expand Down
Loading