From 487ea1e6d31532192fe1996f780c238086485597 Mon Sep 17 00:00:00 2001 From: Taus Date: Wed, 11 Nov 2026 12:52:03 +0000 Subject: [PATCH] yeast: Accept ranges in location overrides Allow `location:` blocks to yield `Range` and `Option` values directly. To do this, we get rid of `IntoLocationRanges`, and instead add a new trait called `LocationSource`. Anything that can provide a location implements this trait, including anything that is already a `ContextNodeId` (for which there previously was a blanket implementation of `IntoLocationRanges`).. This allows us to make `Range` implement `LocationSource`, which means we can put a range in the `location` field and have it automatically get included in the location override calculation. --- shared/yeast-macros/src/lower.rs | 6 +++- shared/yeast/doc/yeast.md | 8 +++++ shared/yeast/src/build.rs | 8 ++--- shared/yeast/src/lib.rs | 28 +++++++++------ shared/yeast/src/range.rs | 9 +++++ shared/yeast/tests/test.rs | 61 ++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 15 deletions(-) diff --git a/shared/yeast-macros/src/lower.rs b/shared/yeast-macros/src/lower.rs index 0aaa4c38e95f..8fa4662374e2 100644 --- a/shared/yeast-macros/src/lower.rs +++ b/shared/yeast-macros/src/lower.rs @@ -545,7 +545,11 @@ impl Node { quote! { let __value = ::std::iter::IntoIterator::into_iter(#block) .collect::>(); - #ctx.extend_location_ranges(__value, &mut __location_ranges); + __location_ranges.extend( + __value + .into_iter() + .filter_map(|__source| #ctx.location_source_range(__source)), + ); }, )); } diff --git a/shared/yeast/doc/yeast.md b/shared/yeast/doc/yeast.md index 99d3424c1a52..3a67f3041c43 100644 --- a/shared/yeast/doc/yeast.md +++ b/shared/yeast/doc/yeast.md @@ -297,6 +297,14 @@ leaf nodes: (identifier "synthetic" location: {source_node}) ``` +The block may also yield a `Range` or `Option` directly, allowing an +exact or computed source range: + +```rust +(identifier "synthetic" + location: {ctx.source_range(source_node).map(|range| range.empty_at_end())}) +``` + The override applies only to the node carrying the `location` field; nested nodes still derive their locations normally. diff --git a/shared/yeast/src/build.rs b/shared/yeast/src/build.rs index e34c4fe78f9e..c0943a04b09f 100644 --- a/shared/yeast/src/build.rs +++ b/shared/yeast/src/build.rs @@ -1,8 +1,8 @@ use std::collections::{BTreeMap, BTreeSet}; use crate::{ - Ast, ContextNodeId, FieldId, Id, IntoFieldIds, IntoLocationRanges, KindId, NodeContent, - OutputAst, OutputId, Range, TranslatorHandle, + Ast, ContextNodeId, FieldId, Id, IntoFieldIds, KindId, LocationSource, NodeContent, OutputAst, + OutputId, Range, TranslatorHandle, }; /// Context for building new AST nodes during a transformation. @@ -200,8 +200,8 @@ impl<'a, C> BuildCtx<'a, C> { } #[doc(hidden)] - pub fn extend_location_ranges>(&self, value: V, out: &mut Vec) { - value.extend_ranges(self, out); + pub fn location_source_range(&self, source: S) -> Option { + source.location_range(self) } /// Return an empty range between two non-overlapping nodes. diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 717ed381aaa1..e34fa1490d79 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -236,18 +236,26 @@ impl ContextNodeId for &T { } } -/// Values accepted by `location: { ... }` template entries. -pub trait IntoLocationRanges { - fn extend_ranges(self, ctx: &build::BuildCtx<'_, C>, out: &mut Vec); +#[doc(hidden)] +pub trait LocationSource: Copy { + fn location_range(self, ctx: &build::BuildCtx<'_, C>) -> Option; } -impl IntoLocationRanges for I -where - I: IntoIterator, - T: ContextNodeId, -{ - fn extend_ranges(self, ctx: &build::BuildCtx<'_, C>, out: &mut Vec) { - out.extend(self.into_iter().filter_map(|id| id.source_range(ctx))); +impl LocationSource for T { + fn location_range(self, ctx: &build::BuildCtx<'_, C>) -> Option { + ContextNodeId::source_range(self, ctx) + } +} + +impl LocationSource for Range { + fn location_range(self, _ctx: &build::BuildCtx<'_, C>) -> Option { + Some(self) + } +} + +impl LocationSource for &Range { + fn location_range(self, _ctx: &build::BuildCtx<'_, C>) -> Option { + Some(*self) } } diff --git a/shared/yeast/src/range.rs b/shared/yeast/src/range.rs index 6b0024596bb5..bfe3ae695603 100644 --- a/shared/yeast/src/range.rs +++ b/shared/yeast/src/range.rs @@ -31,6 +31,15 @@ pub struct Range { pub end_point: Point, } +impl IntoIterator for Range { + type Item = Range; + type IntoIter = std::iter::Once; + + fn into_iter(self) -> Self::IntoIter { + std::iter::once(self) + } +} + impl Range { /// Return the smallest range containing both ranges. pub fn union(self, other: Self) -> Self { diff --git a/shared/yeast/tests/test.rs b/shared/yeast/tests/test.rs index d802e81e626b..02b0c59f7c43 100644 --- a/shared/yeast/tests/test.rs +++ b/shared/yeast/tests/test.rs @@ -1090,6 +1090,23 @@ fn test_interpolation_accepts_value_borrowed_from_context() { yeast::tree!((call location: {locations})); } +#[test] +fn test_location_interpolation_materializes_context_iterator() { + let input = "x = 1"; + let input_ast = parse_input(input); + let mut ast = empty_output_ast(input); + let mut user_ctx = BorrowedInterpolationContext::default(); + let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx); + let location = + yeast::tree!((identifier "location" location: {input_ast.get_root()})); + ctx.locations.push(location); + + let call = yeast::tree!((call location: {ctx.locations.drain(..)})); + + assert_eq!(ctx.locations, vec![location]); + assert_eq!(ctx.output.get_node(call).unwrap().byte_range(), 0..5); +} + #[test] fn test_root_choice_returns_an_optional_node() { let input = "x = 1"; @@ -2268,6 +2285,50 @@ fn test_location_field_is_available_on_literal_nodes() { assert_eq!(identifier.byte_range(), 4..5); } +#[test] +fn test_location_field_accepts_ranges() { + let rule: Rule = rule!( + (assignment + left: (identifier) @left + right: (integer) @right) + => + (identifier "synthetic" + location: {ctx.source_range(right).unwrap().empty_at_end()}) + ); + + let ast = run_and_ast("x = 1", vec![rule]); + let identifier = ast + .reachable_node_ids() + .into_iter() + .filter_map(|id| ast.get_node(id)) + .find(|node| node.kind_name() == "identifier") + .expect("identifier exists"); + + assert_eq!(identifier.byte_range(), 5..5); +} + +#[test] +fn test_location_field_accepts_optional_ranges() { + let rule: Rule = rule!( + (assignment + left: (identifier) @left + right: (integer) @right) + => + (identifier "synthetic" + location: {ctx.source_range(right).map(|range| range.empty_at_end())}) + ); + + let ast = run_and_ast("x = 1", vec![rule]); + let identifier = ast + .reachable_node_ids() + .into_iter() + .filter_map(|id| ast.get_node(id)) + .find(|node| node.kind_name() == "identifier") + .expect("identifier exists"); + + assert_eq!(identifier.byte_range(), 5..5); +} + #[test] fn test_ignored_location_field_is_excluded_from_rule_result_location() { let rule: Rule = rule!(