Skip to content
Merged
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: 0 additions & 2 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,6 @@ pub fn trees(input: TokenStream) -> TokenStream {
/// (output_template)
/// )
///
/// // Shorthand: captures become fields on the output node
/// rule!((query ...) => output_kind)
/// ```
///
/// Captures become Rust variables automatically:
Expand Down
72 changes: 3 additions & 69 deletions shared/yeast-macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,8 +768,7 @@ fn extract_captures_inner(
/// ```
///
/// Template bodies (`=> (kind …)`) never carry an annotation — the
/// output kind is the template root. The shorthand `=> kind` (no
/// body) also carries no annotation. See `parse_rule_top` for dispatch.
/// output kind is the template root.
#[derive(Clone, Debug)]
struct ReturnAnnotation {
kind: Ident,
Expand All @@ -793,7 +792,6 @@ enum AnnotationMultiplicity {
/// `kind {` → annotation (single)
/// `kind? {` → annotation (optional)
/// `kind* {` → annotation (repeated)
/// `kind` → shorthand form (no `{` follows) — NOT an annotation
/// anything else → template or bare block — NOT an annotation
fn try_consume_return_annotation(tokens: &mut Tokens) -> Result<Option<ReturnAnnotation>> {
// Must start with an identifier (the kind name).
Expand Down Expand Up @@ -881,15 +879,13 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
let raw_bindings = capture_bindings(raw_captures.into_iter());
let translated_bindings = capture_bindings(translated_captures.into_iter());

// Parse transform: the token(s) after `=>` fall into one of three
// Parse transform: the token(s) after `=>` fall into one of two
// shapes, dispatched in order:
//
// 1. `kind [? | *] { rust_body }` — annotated Rust body (NEW).
// Static-analysis-ready: the annotation declares the output
// kind and multiplicity in the schema's own vocabulary.
// 2. `kind` alone — shorthand: emit `(kind field: {@cap})…` from
// the query's captures.
// 3. anything else — full template form (`(kind …)` or bare
// 2. anything else — full template form (`(kind …)` or bare
// `{ … }` splice via `parse_direct_list`).
let annotation = try_consume_return_annotation(&mut tokens)?;

Expand Down Expand Up @@ -926,65 +922,6 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
yeast::IntoFieldIds::extend_into(__value, &mut __ids);
__ids
}
} else if peek_is_field(&mut tokens) && {
// Shorthand form: bare identifier = output node kind.
// Auto-generate template from captures.
let mut lookahead = tokens.clone();
lookahead.next(); // skip ident
lookahead.peek().is_none() // nothing after = shorthand
} {
let output_kind = expect_ident(&mut tokens, "expected output node kind")?;
let output_kind_str = output_kind.to_string();

// Generate field assignments from captures
let field_stmts: Vec<TokenStream> = captures
.iter()
.map(|cap| {
let name = Ident::new(&cap.name, Span::call_site());
let name_str = &cap.name;
match cap.multiplicity {
CaptureMultiplicity::Repeated => quote! {
let __field_id = #ctx_ident.ast.field_id_for_name(#name_str)
.unwrap_or_else(|| panic!("field '{}' not found", #name_str));
__fields.insert(
__field_id,
#name.into_iter()
.map(::std::convert::Into::<yeast::Id>::into)
.collect(),
);
},
CaptureMultiplicity::Optional => quote! {
let __field_id = #ctx_ident.ast.field_id_for_name(#name_str)
.unwrap_or_else(|| panic!("field '{}' not found", #name_str));
if let Some(__id) = #name {
__fields.entry(__field_id).or_insert_with(Vec::new)
.push(::std::convert::Into::<yeast::Id>::into(__id));
}
},
CaptureMultiplicity::Single => quote! {
let __field_id = #ctx_ident.ast.field_id_for_name(#name_str)
.unwrap_or_else(|| panic!("field '{}' not found", #name_str));
__fields.entry(__field_id).or_insert_with(Vec::new)
.push(::std::convert::Into::<yeast::Id>::into(#name));
},
}
})
.collect();

quote! {
let __kind = #ctx_ident.ast.id_for_node_kind(#output_kind_str)
.unwrap_or_else(|| panic!("node kind '{}' not found", #output_kind_str));
let mut __fields = std::collections::BTreeMap::new();
#(#field_stmts)*
let __id = #ctx_ident.ast.create_node_with_range(
__kind,
yeast::NodeContent::DynamicString(String::new()),
__fields,
true,
__source_range,
);
vec![__id]
}
} else {
// Reject bare `{ ... }` transforms — they used to be accepted
// as either a Rust body producing a `Vec<Id>` or a template
Expand Down Expand Up @@ -1482,8 +1419,5 @@ mod rules_tests {
// Match expressions inside a block: `=>` is inside braces.
let toks = quote! { { match x { 1 => 2, _ => 3 } } };
assert!(!has_top_level_arrow(&toks));
// Bare shorthand form: top-level `=>` followed by a bare ident.
let toks = quote! { (a) => kind };
assert!(has_top_level_arrow(&toks));
}
}
15 changes: 2 additions & 13 deletions shared/yeast/doc/yeast.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ automatically: single captures bind as `Id`, repeated captures (after
## The `rule!` macro

`rule!` combines a query and a transform into a single declaration.
There are three transform forms, each suited to a different level of
There are two transform forms, each suited to a different level of
rule complexity:

```rust
Expand All @@ -433,13 +433,7 @@ yeast::rule!(
(output_template field: {capture})
)

// 2. Shorthand form — captures become fields on a bare output kind.
yeast::rule!(
(query_pattern field: (_) @capture)
=> output_kind
)

// 3. Annotation form — a Rust block body preceded by the output kind.
// 2. Annotation form — a Rust block body preceded by the output kind.
yeast::rule!(
(query_pattern child: (_)+ @@children)
=>
Expand All @@ -455,9 +449,6 @@ yeast::rule!(
)
```

The shorthand `=> kind` form auto-generates the template, mapping each
capture name to a field of the same name on the output node.

### Guards

A rule may include a Rust guard between its query and `=>`. The guard runs
Expand Down Expand Up @@ -537,8 +528,6 @@ having to inspect the block's expression.
Prefer the simplest form that fits:

- If the whole transform is a tree literal, use the **template form**.
- If the transform is a template whose root matches a query capture
1:1, use the **shorthand form**.
- If the transform needs Rust logic (loops, `let` bindings, calls to
`ctx.translate`, etc.), use the **annotation form**.

Expand Down
89 changes: 8 additions & 81 deletions shared/yeast/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -896,28 +896,6 @@ fn test_desugar_for_loop() {
);
}

#[test]
fn test_shorthand_rule() {
let rule: Rule = yeast::rule!(
(assignment
left: (_) @method
right: (_) @receiver
)
=> call
);

let dump = run_and_dump("x = 1", vec![rule]);
assert_dump_eq(
&dump,
r#"
program
call
method: identifier "x"
receiver: integer "1"
"#,
);
}

#[derive(Clone, Default)]
struct GuardTestContext {
enabled: bool,
Expand Down Expand Up @@ -1024,19 +1002,19 @@ fn test_chained_rules_output_only_kind() {
// first_node → second_node (output-only → output-only)
// The matcher must look up `first_node` against the schema, which only
// knows about it via the YAML node-types file.
let assignment_to_first = yeast::rule!(
let assignment_to_first: Rule = yeast::rule!(
(assignment
left: (_) @left
right: (_) @right
)
=> first_node
=> (first_node left: {left} right: {right})
);
let first_to_second = yeast::rule!(
let first_to_second: Rule = yeast::rule!(
(first_node
left: (_) @left
right: (_) @right
)
=> second_node
=> (second_node left: {left} right: {right})
);

let dump = run_and_dump("x = 1", vec![assignment_to_first, first_to_second]);
Expand Down Expand Up @@ -1103,19 +1081,19 @@ fn test_phased_desugaring() {
// Two phases that could equally have been a single one with chained
// rules. Splitting them makes the intent (cleanup, then desugar)
// explicit and provides per-phase error messages.
let cleanup = vec![yeast::rule!(
let cleanup: Vec<Rule> = vec![yeast::rule!(
(assignment
left: (_) @left
right: (_) @right
)
=> first_node
=> (first_node left: {left} right: {right})
)];
let desugar = vec![yeast::rule!(
let desugar: Vec<Rule> = vec![yeast::rule!(
(first_node
left: (_) @left
right: (_) @right
)
=> second_node
=> (second_node left: {left} right: {right})
)];

let dump = run_phased_and_dump(
Expand Down Expand Up @@ -1731,33 +1709,6 @@ fn test_rules_macro_accepts_bare_rule_body() {
);
}

/// The bare-rule-body shorthand `=> output_kind` should also be accepted.
#[test]
fn test_rules_macro_accepts_bare_shorthand_form() {
let rules: Vec<Rule> = yeast::rules! {
input: "tests/input-types.yml",
output: "tests/node-types.yml",
[
(assignment
left: (_) @method
right: (_) @receiver
)
=> call,
]
};

let dump = run_and_dump("x = 1", rules);
assert_dump_eq(
&dump,
r#"
program
call
method: identifier "x"
receiver: integer "1"
"#,
);
}

#[test]
fn test_rules_macro_accepts_bare_guarded_rule() {
let rules: Vec<Rule> = yeast::rules! {
Expand Down Expand Up @@ -1926,27 +1877,3 @@ fn test_rule_annotation_single() {
}
assert!(has_assignment, "expected an assignment node");
}

/// The shorthand `=> kind` form (no body, no annotation) must still be
/// distinguished from the annotation form and continue to work.
#[test]
fn test_shorthand_still_works_alongside_annotation_syntax() {
let r: Rule = rule!(
(assignment left: (_) @method right: (_) @receiver)
=>
call
);
let ast = run_and_ast("x = 1", vec![r]);
let mut has_call = false;
for id in ast.reachable_node_ids() {
if let Some(n) = ast.get_node(id) {
if n.kind_name() == "call" {
has_call = true;
}
}
}
assert!(
has_call,
"shorthand form should still produce a `call` node"
);
}
Loading