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
6 changes: 3 additions & 3 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ pub fn rule(input: TokenStream) -> TokenStream {
///
/// 1. A **bare rule body** `(query) => (template)` — the `rule!(...)`
/// wrapper is implicit.
/// 2. An explicit `rule!(...)` invocation, possibly chained as
/// `rule!(...).repeated()` or path-prefixed as `yeast::rule!(...)`.
/// 2. An explicit `rule!(...)` invocation, possibly path-prefixed as
/// `yeast::rule!(...)`.
/// 3. Any other expression returning a `Rule` (helper-function calls,
/// conditionals).
///
Expand All @@ -176,7 +176,7 @@ pub fn rule(input: TokenStream) -> TokenStream {
/// [
/// (source_file (_)* @cs) => (top_level body: {..cs}),
/// (simple_identifier) @id => (name_expr identifier: (identifier #{id})),
/// rule!((integer_literal) @lit => (int_literal #{lit})).repeated(),
/// rule!((integer_literal) @lit => (int_literal #{lit})),
/// helper_fn(),
/// ]
/// };
Expand Down
7 changes: 2 additions & 5 deletions shared/yeast-macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -970,9 +970,6 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
// captured node before invoking the user's transform body,
// except for `@@name` captures listed in `__skip` which the
// body consumes raw.
// For OneShot rules this preserves the legacy behaviour
// (input-schema captures translated to output-schema
// nodes); for Repeating rules it is a no-op.
let __skip: &[&str] = &[#(#raw_capture_names),*];
__translator.auto_translate_captures(&mut __captures, __ast, __user_ctx, __skip)?;
#(#raw_bindings)*
Expand Down Expand Up @@ -1141,8 +1138,8 @@ fn expect_repetition(tokens: &mut Tokens) -> Result<TokenStream> {
/// Each item in the bracketed list can be:
/// * a **bare rule body** `(query) => (template)` — wrapped implicitly
/// in `yeast::rule! { ... }` for codegen;
/// * an explicit `rule!(...)` (or `rule!(...).repeated()`,
/// `yeast::rule!(...)`, etc.) — passed through verbatim;
/// * an explicit `rule!(...)` (including `yeast::rule!(...)`) — passed
/// through verbatim;
/// * any other expression returning a `Rule` (helper-function calls,
/// conditionals) — passed through verbatim.
///
Expand Down
70 changes: 25 additions & 45 deletions shared/yeast/doc/yeast.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,18 @@ A YEAST `Rule` has two parts:
pattern language.
2. A **transform** that produces replacement nodes from the match captures.

The `Runner` applies rules by walking the tree top-down. At each node, it
tries each rule in order. If a rule's query matches, the node is replaced by
the transform's output, and the rules are re-applied to the result. If no
rule matches, the node is kept and its children are processed recursively.
The `Runner` translates the root with the first matching rule. Before the
rule's transform runs, captured input nodes are recursively translated with
the same rules. Every visited input node must match a rule; a missing match is
an error.

A rule can replace one node with zero nodes (deletion), one node (rewriting),
or multiple nodes (expansion).

By default a rule fires **at most once on a given node**: after firing, the
engine will not re-try that same rule on the result root. Other rules may
still fire on the result, and the rule may still fire on different nodes
(including the result's children). To opt into iterative behaviour — when a
rule's output is intentionally re-matched by the same rule — call
`.repeated()` on the constructed `Rule`:

```rust
let r = yeast::rule!((foo ...) => (foo ...)).repeated();
```

Without `.repeated()`, a rule whose output happens to match its own query
simply fires once and stops. With `.repeated()`, the rule is allowed to
re-match indefinitely; the runner still enforces a global rewrite-depth
limit (currently 100) as a safety net against accidental cycles.
A rule's output is final for the current phase: it is not matched or traversed
again. This allows rule queries to describe the input schema while transforms
build nodes from a different output schema. Use another named phase when the
output of one exhaustive translation must become the input to another.

## Query language

Expand Down Expand Up @@ -429,10 +418,9 @@ rule!(

### Raw captures (`@@name`)

The default `@name` capture marker is *auto-translated*: in OneShot
phases the macro recursively translates the captured node before
binding it, so `{name}` in the output template splices a node that
already conforms to the output schema.
The default `@name` capture marker is *auto-translated*: the macro recursively
translates the captured node before binding it, so `{name}` in the output
template splices a node that already conforms to the output schema.

For rules that need the raw (input-schema) capture — typically to read
its source text or to translate it explicitly with mutable context
Expand All @@ -456,9 +444,7 @@ yeast::rule!(
);
```

Mix `@` and `@@` freely in the same rule. In a Repeating phase both
markers are equivalent (auto-translation is a no-op for repeating
rules).
Mix `@` and `@@` freely in the same rule.

## The `rule!` macro

Expand Down Expand Up @@ -574,18 +560,17 @@ Prefer the simplest form that fits:

## Integration with the extractor

A YEAST desugaring pass is configured with a [`DesugaringConfig`], which
carries one or more named [`Phase`]s of rules and an optional output
node-types schema (in YAML format). Each phase is a complete traversal
that runs to completion before the next phase starts; only the current
phase's rules are considered during that traversal. Attach the config to
a language spec
to enable rewriting:
A YEAST translation pass is configured with a [`DesugaringConfig`], which
carries one or more named [`Phase`]s of exhaustive rules and an optional
output node-types schema (in YAML format). Each phase translates the previous
phase's root before the next phase starts; only the current phase's rules are
considered during that translation. Attach the config to a language spec to
enable rewriting:

```rust
let desugar = yeast::DesugaringConfig::new()
.add_phase("cleanup", yeast::PhaseKind::Repeating, cleanup_rules())
.add_phase("translate", yeast::PhaseKind::OneShot, translate_rules())
.add_phase("normalize", normalization_rules())
.add_phase("translate", translation_rules())
.with_output_node_types_yaml(include_str!("output-node-types.yml"));

let lang = simple::LanguageSpec {
Expand All @@ -600,14 +585,10 @@ let lang = simple::LanguageSpec {
A single-phase config is just `.add_phase(...)` called once. Phase names
appear in error messages so you can tell which phase failed.

There are two kinds of phases:
- **Repeating**:
Each node is re-processed until none of the rules in the phase matches.
When a node no longer matches any rules, its children are recursively processed. In practice this is used to desugar or simplify an AST, while staying mostly within the same schema.
- **One-shot**:
Each node is processed by the first matching rule, and the engine panics if no rule matches.
Rules are then recursively applied to every captured node.
In practice this is used when translating from one AST schema to another, where an exhaustive match is required.
Every phase uses one-shot translation: each visited input node is processed by
the first matching rule, captured nodes are recursively translated, and the
phase errors if no rule matches. Output nodes are not reprocessed in the same
phase.

The same YAML node-types is used for both the runtime yeast `Schema` (so
rules can refer to output-only kinds and fields) and TRAP validation (it
Expand Down Expand Up @@ -645,8 +626,7 @@ let translation_rules: Vec<yeast::Rule> = yeast::rules! {
Each comma-separated item in the bracketed list may be:

- A **bare rule body** `(query) => (template)` — no `rule!(...)` wrapper.
- An explicit `rule!(...)` invocation, with optional postfix calls such
as `rule!(...).repeated()`.
- An explicit `rule!(...)` invocation.
- Any other expression returning a `Rule` (helper functions, etc.).

Schema paths are resolved relative to the consuming crate's
Expand Down
6 changes: 2 additions & 4 deletions shared/yeast/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,8 @@ impl<'a, C> BuildCtx<'a, C> {

impl<C: Clone> BuildCtx<'_, C> {
/// Recursively translate every id in the given iterable via the
/// framework's rule machinery. In a OneShot phase, applies OneShot
/// rules to each id and returns the accumulated resulting node ids
/// in order. In a Repeating phase, errors (translation is not
/// meaningful when input and output share a schema).
/// framework's rule machinery and return the accumulated resulting node
/// ids in order.
///
/// The single-`Id` case works too, because `Id: IntoIterator<Item
/// = Id>` is a singleton iterator — so `ctx.translate(some_id)?`
Expand Down
Loading
Loading