Skip to content

Recursive SpannedTypeExpr for precise type-error diagnostics#3220

Open
hellovai wants to merge 4 commits intocanaryfrom
hellovai/lsp-diagnostics
Open

Recursive SpannedTypeExpr for precise type-error diagnostics#3220
hellovai wants to merge 4 commits intocanaryfrom
hellovai/lsp-diagnostics

Conversation

@hellovai
Copy link
Contributor

@hellovai hellovai commented Mar 6, 2026

Before this change, type expressions were stored as a single TypeExpr plus one span, so diagnostics (e.g. "unresolved type") pointed at the whole annotation. Now every sub-expression has its own span, so we can highlight only the part that's wrong.

Examples of impact

  • Union with one bad member: For x: int | sring | bool, we now underline only sring (e.g. 14..20) with "unresolved type: sring" instead of the whole int | sring | bool.

  • Bad return type: For function f() -> DoesNotExist { ... }, the error is on DoesNotExist (e.g. 16..28), not the entire -> DoesNotExist.

  • Missing return: For a function declared -> int with no return, the "missing return" diagnostic is on the return type annotation : int (e.g. 16..19), not the block body, so it's clear which function is wrong.

  • Parameter type: For fn f(x: Nonexistent), the diagnostic targets the Nonexistent token (e.g. 11..25), not the whole parameter.

What changed

  • AST: SpannedTypeExpr is recursive (SpannedTypeExprKind with spanned children). Add to_type_expr() for span-free TypeExpr.
  • CST→AST: Type lowering produces SpannedTypeExpr with per-node trimmed_text_range(); lower_cst uses it directly.
  • HIR: SignatureSourceMap stores full SpannedTypeExpr trees for params, return, and throws.
  • TIR: Add lower_spanned_type_expr(), report (TirTypeError, span) per node; use for return/param/field/alias. Report "missing return" on return-type span via return_type_span.
  • LSP: check uses lower_spanned_type_expr for precise spans.
  • Syntax: UnionMemberParts::text_range(), TypeExpr::trimmed_text_range().
  • Tests and tools_onionskin updated; UnresolvedType message uses backticks.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Type annotations now record and preserve precise per-subexpression spans for richer source mapping.
  • Bug Fixes

    • Diagnostics for return, parameter, throws and other type errors now report exact sub-expression locations and use improved code-formatted type names.
    • Many tooling paths updated to emit span-aware diagnostics for clearer error placement.
  • Tests

    • Added and updated tests for unknown types and span-accurate diagnostics (union, throws, let bindings).

Before this change, type expressions were stored as a single TypeExpr plus
one span, so diagnostics (e.g. "unresolved type") pointed at the whole
annotation. Now every sub-expression has its own span, so we can highlight
only the part that's wrong.

Examples of impact:

- Union with one bad member: For `x: int | sring | bool`, we now underline
  only `sring` (e.g. 14..20) with "unresolved type: `sring`" instead of
  the whole `int | sring | bool`.

- Bad return type: For `function f() -> DoesNotExist { ... }`, the error is
  on `DoesNotExist` (e.g. 16..28), not the entire `-> DoesNotExist`.

- Missing return: For a function declared `-> int` with no return, the
  "missing return" diagnostic is on the return type annotation `: int`
  (e.g. 16..19), not the block body, so it's clear which function is wrong.

- Parameter type: For `fn f(x: Nonexistent)`, the diagnostic targets the
  `Nonexistent` token (e.g. 11..25), not the whole parameter.

What changed:

- AST: SpannedTypeExpr is recursive (SpannedTypeExprKind with spanned
  children). Add to_type_expr() for span-free TypeExpr.
- CST→AST: Type lowering produces SpannedTypeExpr with per-node
  trimmed_text_range(); lower_cst uses it directly.
- HIR: SignatureSourceMap stores full SpannedTypeExpr trees for params,
  return, and throws.
- TIR: Add lower_spanned_type_expr(), report (TirTypeError, span) per node;
  use for return/param/field/alias. Report "missing return" on return-type
  span via return_type_span.
- LSP: check uses lower_spanned_type_expr for precise spans.
- Syntax: UnionMemberParts::text_range(), TypeExpr::trimmed_text_range().
- Tests and tools_onionskin updated; UnresolvedType message uses backticks.

Made-with: Cursor
@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Mar 7, 2026 4:00am
promptfiddle Ready Ready Preview, Comment Mar 7, 2026 4:00am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Adds a recursive, span-bearing TypeExpr (SpannedTypeExpr) produced by CST->AST lowering, stores spanned trees in source/signature maps, converts to plain TypeExpr via to_type_expr(), and introduces span-aware lowering to Ty that collects diagnostics with precise TextRange locations.

Changes

Cohort / File(s) Summary
Spanned AST Types
baml_language/crates/baml_compiler2_ast/src/ast.rs
Adds SpannedTypeExprKind, SpannedTypeExpr, SpannedFunctionTypeParam, changes SpannedTypeExpr.expr -> kind, adds SpannedTypeExpr::to_type_expr(), and updates Pattern::TypedBinding to carry SpannedTypeExpr.
Lowering: CST -> Spanned AST
baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
Refactors lowering to return SpannedTypeExpr everywhere, preserving per-subexpression TextRange; adds lower_from_type_name(name, span) and updates signatures to return spanned types.
Lowering: CST usage / allocations
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
alloc_type_annot now accepts SpannedTypeExpr and records both the erased TypeExpr and the original spanned node; call sites updated to pass SpannedTypeExpr and use spanned.span.
AST public field updates
baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
Some public struct fields revert to plain TypeExpr after lowering (e.g., FunctionDef.return_type, Param.type_expr, FieldDef.type_expr, TypeAliasDef.type_expr).
AST consumers / small updates
baml_language/crates/baml_compiler2_ast/src/lib.rs
Replaces direct .expr accesses with to_type_expr() and updates tests to match SpannedTypeExprKind where needed.
Signature mapping (HIR)
baml_language/crates/baml_compiler2_hir/src/signature.rs
Extends SignatureSourceMap to store param_type_exprs: Vec<Option<SpannedTypeExpr>>, return_type_expr: Option<SpannedTypeExpr>, throws_type_expr: Option<SpannedTypeExpr> and converts to TypeExpr via to_type_expr() for semantic signatures.
TIR: span-aware lowering
baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs
Adds lower_spanned_type_expr that lowers SpannedTypeExpr -> Ty, collecting per-node diagnostics as (TirTypeError, TextRange) and attaching precise spans to errors.
TIR: inference & builder
baml_language/crates/baml_compiler2_tir/src/inference.rs, baml_language/crates/baml_compiler2_tir/src/builder.rs
Uses spanned source-map entries, lowers via lower_spanned_type_expr, propagates diagnostics with spans; TypeInferenceBuilder gains return_type_span and body_source_map plus setters (set_return_type_span, set_body_source_map) and uses span info for diagnostics (e.g., MissingReturn).
TIR: error formatting
baml_language/crates/baml_compiler2_tir/src/infer_context.rs
Formats UnresolvedType messages with backticks around the type name.
LSP / Check actions
baml_language/crates/baml_lsp2_actions/src/check.rs
Emits per-node diagnostics by running lower_spanned_type_expr on function returns, parameters, and throws; replaces whole-field diagnostics with per-span diagnostics.
Formatting consumer
baml_language/crates/tools_onionskin/src/compiler.rs
Adds helper to stringify SpannedTypeExpr via to_type_expr() and replaces .expr usages in HIR2 formatting paths.
Syntax helpers
baml_language/crates/baml_compiler_syntax/src/ast.rs
Adds UnionMemberParts::text_range() and TypeExpr::trimmed_text_range() to compute meaningful token ranges used by lowering.
Tests / expectations
baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs, baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
Adjusts diagnostic messages to use backticks and refined spans; adds tests for unknown types in unions/throws/let bindings; updates pattern formatting to call ty.to_type_expr().

Sequence Diagram(s)

sequenceDiagram
  participant CST as CST (parser)
  participant ASTL as AST Lowerer
  participant Spanned as SpannedTypeExpr (AST)
  participant HIR as HIR Signature/SourceMap
  participant TIRL as TIR Lowerer
  participant Builder as TypeInferenceBuilder
  participant Reporter as Diagnostics Reporter / LSP

  CST->>ASTL: provide CstTypeExpr
  ASTL->>Spanned: lower to SpannedTypeExpr (recursive, per-node spans)
  Spanned->>HIR: store SpannedTypeExpr in SignatureSourceMap / AstSourceMap
  HIR->>Builder: supply SpannedTypeExpr entries
  Builder->>TIRL: call lower_spanned_type_expr(spanned)
  TIRL->>Builder: return Ty and (error, TextRange) diagnostics
  Builder->>Reporter: report diagnostics at provided TextRange
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • fixed let annotations #3115: Modifies type-checking diagnostics to carry and report span information for type expressions, aligning with this PR's span-aware diagnostic propagation strategy.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing recursive SpannedTypeExpr to enable precise per-node type-error diagnostics.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hellovai/lsp-diagnostics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 6, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 15 untouched benchmarks
⏩ 91 skipped benchmarks1


Comparing hellovai/lsp-diagnostics (d0c59b8) with canary (7b8db01)

Open in CodSpeed

Footnotes

  1. 91 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Binary size checks passed

7 passed

Artifact Platform Gzip Baseline Delta Status
bridge_cffi Linux 4.4 MB 4.4 MB +6.3 KB (+0.1%) OK
bridge_cffi-stripped Linux 2.9 MB 2.9 MB +5.7 KB (+0.2%) OK
bridge_cffi macOS 3.6 MB 3.6 MB +6.3 KB (+0.2%) OK
bridge_cffi-stripped macOS 2.3 MB 2.3 MB +3.7 KB (+0.2%) OK
bridge_cffi Windows 3.6 MB 3.6 MB +4.8 KB (+0.1%) OK
bridge_cffi-stripped Windows 2.4 MB 2.4 MB +5.1 KB (+0.2%) OK
bridge_wasm WASM 2.2 MB 2.2 MB +4.1 KB (+0.2%) OK

Generated by cargo size-gate · workflow run

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aacb7e5d-a876-4d09-af37-c9c1f2652992

📥 Commits

Reviewing files that changed from the base of the PR and between f32fa20 and 5a75524.

📒 Files selected for processing (14)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_hir/src/signature.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/infer_context.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_compiler2_tir/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler_syntax/src/ast.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

- Fix use-after-move in lib.rs test (match instead of matches! + panic)
- Distinct spans for synthetic container levels (lower_type_expr + test)
- Skip field diagnostics in lookup_class_fields (avoid duplicates)
- check_throws_contract: use throws_type_expr + lower_spanned_type_expr
- LSP check: validate throws with lower_spanned_type_expr
- Onionskin: hir2_spanned_type_expr_to_string helper
- phase3a: unknown_type_in_union_return, unknown_type_in_throws

Made-with: Cursor
…agnostics)

- AstSourceMap: add type_annotation_spanned_exprs + type_annotation_spanned(id)
- alloc_type_annot: take SpannedTypeExpr, store ty + span + spanned
- Pattern::TypedBinding: ty is SpannedTypeExpr; consumers use .to_type_expr()
- TIR: set_body_source_map from function_body_source_map; in check_stmt(Let)
  use lower_spanned_type_expr when spanned available, report (diag, span) per node
- Tests: unknown_type_in_let_binding, unknown_type_in_let_binding_union
  snapshots updated for precise spans (e.g. 35..40 for sring only)

Made-with: Cursor
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs (1)

43-47: ⚠️ Potential issue | 🟠 Major

Wrapper span math still drifts off the real modifier tokens.

Line 44 records [] as a 1-byte span, and Lines 67-74 and 290-299 then build later wrappers from result.span.end(). After the first synthetic wrap, that end no longer matches the actual source suffix, so Foo[]? can attach the optional span to ], and a flat int[][] path can even step past full_span.end(). Please derive each synthetic container span from the original source range (or a cumulative sub-expression range) instead of chaining through prior synthetic spans, and tighten the regression test to assert exact ranges/slices rather than just !=.

As per coding guidelines, Prefer writing Rust unit tests over integration tests where possible.

Also applies to: 67-74, 289-299


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cb03c42-1995-4fbd-b33b-d39b4190680d

📥 Commits

Reviewing files that changed from the base of the PR and between 5a75524 and 7311161.

📒 Files selected for processing (7)
  • baml_language/crates/baml_compiler2_ast/src/lib.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_type_expr.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_lsp2_actions/src/check.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml_compiler2_tir/src/builder.rs (1)

967-975: ⚠️ Potential issue | 🟡 Minor

Report invalid catch-binding types at the annotation span.

Pattern::TypedBinding.ty already has the exact TextRange, but this still anchors InvalidCatchBindingType to base_expr_id, so the underline lands on the catch expression instead of the banned type.

🩹 Precise-span fix
-                        self.context.report_simple(
-                            TirTypeError::InvalidCatchBindingType {
-                                type_name: banned.to_string(),
-                            },
-                            base_expr_id,
-                        );
+                        self.context.report_at_span(
+                            TirTypeError::InvalidCatchBindingType {
+                                type_name: banned.to_string(),
+                            },
+                            ty.span,
+                        );
♻️ Duplicate comments (2)
baml_language/crates/baml_compiler2_tir/src/builder.rs (2)

415-422: ⚠️ Potential issue | 🟠 Major

Scope MissingReturn’s return-type span to the root function body only.

check_expr is used for every block expression. With the builder-wide return_type_span, nested blocks like let x: int = { ... } will still blame the function’s -> T span instead of the offending block/check site.


1249-1250: ⚠️ Potential issue | 🟠 Major

Typed match/catch bindings still erase their recursive spans before lowering.

These ty.to_type_expr() calls undo the new Pattern::TypedBinding storage, so unresolved or malformed types in typed match/catch bindings still report on the arm body instead of the bad type token. Lower the stored SpannedTypeExpr directly and forward its (diag, span) results.

💡 Minimal call-site change
-                self.lower_pattern_type_expr(&ty.to_type_expr(), at_expr)
+                self.lower_spanned_pattern_type_expr(ty, at_expr)
-                let lowered = self.lower_pattern_type_expr(&ty.to_type_expr(), at_expr);
+                let lowered = self.lower_spanned_pattern_type_expr(ty, at_expr);

Also applies to: 1407-1408


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5f169d01-83ff-4938-aa1a-d748a6f5b110

📥 Commits

Reviewing files that changed from the base of the PR and between 7311161 and d0c59b8.

📒 Files selected for processing (7)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_compiler2_tir/src/builder.rs
  • baml_language/crates/baml_compiler2_tir/src/inference.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/mod.rs
  • baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs
  • baml_language/crates/tools_onionskin/src/compiler.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant