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
1 change: 1 addition & 0 deletions baml_language/Cargo.lock

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

8 changes: 7 additions & 1 deletion baml_language/TYPE_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ A declaration (class, enum, interface) and a type are different things. A declar

A package has no intrinsic name. It may declare one (`[package].name`, or the fixed name of a standard-library package), but that is display metadata and the default a dependent spells it by, not its identity. Names live on dependency edges: a package reaches another under the name its own manifest gives that edge, so two packages may reach one package under different names, and a package can name only itself, the packages it lists, and the prelude (the implicitly available standard-library packages). Everything else is invisible to it. Today (09/2026) a user package declares no dependencies and reaches exactly the prelude.

Source always references declarations fully namespaced as `<package>.<namespace*>.<item>` where `<package>` is the name of a dependency graph edge (including `root` which is always the edge to its own package, similar to Rust's `crate`). `root` is reserved: no item, binding, type parameter, or namespace may be named `root`. The only exception is when referencing local items within the same namespace, which may be referenced by their item name (`<item>`) alone. An item of the writer's own namespace spelled like a package name shadows that package entirely: `<item>.<member>` reads the item, and the package is unreachable by that name from that namespace.
Source always references declarations fully namespaced as `<package>.<namespace*>.<item>` where `<package>` is the name of a dependency graph edge (including `root` which is always the edge to its own package, similar to Rust's `crate`). `root` is a keyword, so nothing can be named `root`. The only exception is when referencing local items within the same namespace, which may be referenced by their item name (`<item>`) alone. An item of the writer's own namespace spelled like a package name shadows that package entirely: `<item>.<member>` reads the item, and the package is unreachable by that name from that namespace.

Three kinds of names are reserved:

- **Keywords** (`class`, `function`, `if`, `let`, `const`, `is`, `true`, `false`, `null`, `self`, `root`, ...: every keyword token of the lexer) never name a declaration, binding, or parameter. After a `.` they name members and path segments, which only the standard library may declare (`t.implements(u)`, `baml.spawn`).
- **Built-in type names** (`int`, `string`, `map`, `json`, `image`, `never`, ...: every single-word spelling in the compiler alias table) cannot name an item, namespace, type parameter, or type binding, since a bare reference reads the built-in type. They stay legal as fields, methods, enum variants, parameters, and locals.
- **Weak keywords** (`client`, `test`, `testset`, `retry_policy`, `template_string`, `generator`, `type`, `as`, `with`) are ordinary identifiers except where they begin their own syntax.

Every rendering of a type is from a viewpoint: the canonical rendering spells every package fully; the user-facing rendering elides the viewer's own package and writes `root.` for its other namespaces. Canonical order of union members and throw sets is deterministic within a program but is not a stable key across programs; nothing may persist it as an identity.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ class Plan<T, E> {
/// Cancelling any of these tokens cancels the task.
cancel_tokens: CancelToken[],
/// Whether the task keeps running when its spawner is cancelled.
root: bool,
is_root: bool,

/// A plan that runs `body`, with nothing added to it.
function new(body: () -> T throws E, name: string? = null) -> Plan<T, E> throws never {
Plan { body: body, name: name, limits: [], cancel_tokens: [], root: false }
Plan { body: body, name: name, limits: [], cancel_tokens: [], is_root: false }
}

/// Returns this plan with a new body: `wrapper`, called with the old one.
Expand All @@ -112,7 +112,7 @@ class Plan<T, E> {
name: self.name,
limits: self.limits,
cancel_tokens: self.cancel_tokens,
root: self.root,
is_root: self.is_root,
}
}

Expand Down Expand Up @@ -195,7 +195,7 @@ implements<T, E> Modifier<T, E> for Limit {

implements<T, E> Modifier<T, E> for Root {
function apply(self, plan: Plan<T, E>) -> Plan<T, E> throws never {
Plan { ...plan, root: true }
Plan { ...plan, is_root: true }
}
}

Expand Down
14 changes: 7 additions & 7 deletions baml_language/crates/baml_compiler2_ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1222,10 +1222,10 @@ class Response {
}

#[test]
fn ast_lowers_keyword_named_class_field() {
fn ast_lowers_weak_keyword_named_class_field() {
let source = r#"
class InterfaceTwo {
interface string
class Session {
client string
}
"#;
let items = parse_and_lower(source);
Expand All @@ -1238,7 +1238,7 @@ class InterfaceTwo {
.expect("expected ClassDef");

let field = class.fields.first().expect("expected field");
assert_eq!(field.name.as_str(), "interface");
assert_eq!(field.name.as_str(), "client");
assert_eq!(field.type_expr.to_string(), "string");
}

Expand Down Expand Up @@ -1287,10 +1287,10 @@ function Foo(t: TypeValue) -> string {
let Expr::Call { callee, .. } = &body.exprs[tail_expr] else {
panic!("expected call expression");
};
let Expr::MemberAccess { member, .. } = &body.exprs[*callee] else {
panic!("expected member access callee");
let Expr::Path(path) = &body.exprs[*callee] else {
panic!("expected path callee");
};
assert_eq!(member.as_str(), "implements");
assert_eq!(path.to_string(), "t.implements");
}

#[test]
Expand Down
49 changes: 4 additions & 45 deletions baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,14 @@ pub struct EnvVarRef {
pub range: TextRange,
}

/// Returns true if `kind` can serve as an identifier token in expression position.
///
/// The parser allows `KW_CLIENT` (and `WORD`) inside `PATH_EXPR` / `FIELD_ACCESS_EXPR`
/// nodes when `client` is used as a variable or field name. It likewise allows
/// `KW_SPAWN` / `KW_AWAIT` as path segments (e.g. the `baml.spawn` namespace),
/// since they are unambiguous after a `.`, and the interface-related keywords
/// (`implements`, `interface`, `extends`) as member names — e.g.
/// `dog_t.implements(animal_t)` on the reflection `type` value. This must
/// match exactly what `parse_path_or_ident` / `at_member_name` accept in the
/// parser; adding a new keyword there requires adding it here too.
/// Returns true if `kind` can serve as an identifier or literal-word token in
/// expression position. The parser emits every name as a `WORD`, keywords
/// used as names included; the remaining kinds lower by text (the
/// literal/identifier arms below switch on it).
pub(crate) fn is_ident_token(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::WORD
| SyntaxKind::KW_CLIENT
| SyntaxKind::KW_SPAWN
| SyntaxKind::KW_AWAIT
| SyntaxKind::KW_CLASS
| SyntaxKind::KW_ENUM
| SyntaxKind::KW_FUNCTION
| SyntaxKind::KW_IMPLEMENTS
| SyntaxKind::KW_IMPLEMENT
| SyntaxKind::KW_INTERFACE
| SyntaxKind::KW_EXTENDS
| SyntaxKind::KW_REQUIRES
// Contextual keywords re-lexed from a `Word`: still lower by text
// (the literal/identifier arms below switch on the text), so they
// must read as ident tokens just as they did when they were `Word`.
| SyntaxKind::KW_AS
| SyntaxKind::KW_TYPE
| SyntaxKind::KW_TRUE
Expand Down Expand Up @@ -1933,10 +1913,6 @@ impl LoweringContext {
SyntaxKind::STRING_LITERAL => Some(Expr::Literal(Literal::String(
strip_string_delimiters(t.text()),
))),
// `spawn`/`await` pass `is_ident_token` (they're
// valid path SEGMENTS) but here they're the keywords
// themselves — never a name/with expression.
SyntaxKind::KW_SPAWN | SyntaxKind::KW_AWAIT => None,
k if is_ident_token(k) => Some(match t.text() {
"true" => Expr::Literal(Literal::Bool(true)),
"false" => Expr::Literal(Literal::Bool(false)),
Expand Down Expand Up @@ -2100,11 +2076,6 @@ impl LoweringContext {
SyntaxKind::CARET => op = Some(BinaryOp::BitXor),
SyntaxKind::LESS_LESS => op = Some(BinaryOp::Shl),
SyntaxKind::GREATER_GREATER => op = Some(BinaryOp::Shr),
SyntaxKind::KW_INSTANCEOF => {
self.diags
.push(LoweringDiagnostic::InstanceofRemoved { span });
return self.alloc_expr(Expr::Missing, node.span_range());
}
SyntaxKind::QUESTION_QUESTION => op = Some(BinaryOp::NullCoalesce),
SyntaxKind::QUESTION if op.is_none() => {
// Two consecutive QUESTION tokens = null coalescing (??)
Expand Down Expand Up @@ -2896,15 +2867,6 @@ impl LoweringContext {
.filter_map(rowan::NodeOrToken::into_token)
.find(|t| t.kind() == SyntaxKind::WORD);

if let Some(token) = &name_token
&& token.text() == "const"
{
self.diags
.push(LoweringDiagnostic::ReservedConstBindingName {
span: token.text_range(),
});
}

let name_span = name_token.as_ref().map(rowan::SyntaxToken::text_range);
let name = name_token.map(|t| Name::new(t.text()));

Expand Down Expand Up @@ -3835,9 +3797,6 @@ impl LoweringContext {
.iter()
.rposition(|token| token.kind() == SyntaxKind::DOT)
.and_then(|dot| tokens.get(dot + 1))
// The full member-name set, not just `WORD`: an interface method
// may be named with a contextual keyword (`implements`, `extends`),
// and the parser accepts those here.
.filter(|token| is_ident_token(token.kind()));
let Some(member_token) = member_token else {
return self.alloc_expr(Expr::Missing, span);
Expand Down
21 changes: 0 additions & 21 deletions baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,6 @@ pub enum LoweringDiagnostic {
/// A byte string literal contains an invalid escape sequence.
InvalidByteStringEscape { message: String, span: TextRange },

/// The `instanceof` operator was used; it has been removed. Use `match` instead.
InstanceofRemoved { span: TextRange },

/// A backtick template has a malformed block tag — an unclosed,
/// mismatched, or stray `${for}`/`${if}` open/close.
MalformedTemplateBlock {
Expand Down Expand Up @@ -141,10 +138,6 @@ pub enum LoweringDiagnostic {
/// `const` currently parses as a non-immutable alias for `let`.
ConstBindingIntroducer { span: TextRange },

/// `const` is reserved as future language surface and cannot be used as a
/// binding name.
ReservedConstBindingName { span: TextRange },

/// An assignment operator (`=`, `+=`, …) appeared in expression position,
/// e.g. `(x = 5)`. Assignment is statement-only in BAML, so the expression
/// has no value; without this diagnostic it would lower to a `Missing` that
Expand Down Expand Up @@ -451,13 +444,6 @@ impl LoweringDiagnostic {
*span,
"invalid escape",
),
LoweringDiagnostic::InstanceofRemoved { span } => (
DiagnosticId::RemovedFeature,
Severity::Error,
"`instanceof` is no longer supported. Use a `match` expression for type checking instead.".to_string(),
*span,
"use `match` instead",
),
LoweringDiagnostic::MalformedTemplateBlock { kind, span } => {
use baml_compiler_syntax::BacktickStructuralErrorKind as K;
let (message, label): (&str, &str) = match kind {
Expand Down Expand Up @@ -552,13 +538,6 @@ impl LoweringDiagnostic {
*span,
"`const` behaves like `let` for now",
),
LoweringDiagnostic::ReservedConstBindingName { span } => (
DiagnosticId::InvalidSyntax,
Severity::Error,
"`const` is reserved and cannot be used as a binding name".to_string(),
*span,
"`const` is reserved here",
),
LoweringDiagnostic::AssignmentInExpressionPosition { span } => (
DiagnosticId::InvalidSyntax,
Severity::Error,
Expand Down
Loading
Loading