Skip to content

Commit a4ae8df

Browse files
committed
yeast: Make rule guard context read-only
Top-level rule guards currently get a mutable copy of the `ctx` user context. If the guard succeeds, this copy is used to replace the existing context and the rule body is executed. However, this design is somewhat problematic: first of all, it's questionable to do such mutation in the first place, and secondly if we ever want to embed guards deeper inside of a rule, then for consistency we would expect these to be mutable as well. This would entail a lot of unnecessary context cloning and bookkeeping for a "feature" that will likely never be used. Thus, I opted to simply make the access to the user context read-only inside of the guard body (as in: we only pass it an immutable reference).
1 parent fd2bfc8 commit a4ae8df

5 files changed

Lines changed: 26 additions & 49 deletions

File tree

‎shared/yeast-macros/src/lib.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,9 @@ pub fn trees(input: TokenStream) -> TokenStream {
158158
/// evaluated after the query matches and before any `@` captures are
159159
/// translated. Returning false makes the driver try the next rule. All
160160
/// captures are therefore raw in the guard; `@` versus `@@` controls only
161-
/// whether the capture is translated for the transform. `ctx` provides
162-
/// mutable user-context access, and `ast` provides read-only AST access.
163-
/// Mutations to `ctx` are visible to the transform when the guard succeeds.
164-
/// Omitting the guard is equivalent to writing `where true`.
161+
/// whether the capture is translated for the transform. Both `ctx` and `ast`
162+
/// provide read-only access. Omitting the guard is equivalent to writing
163+
/// `where true`.
165164
///
166165
/// `tree!` and `trees!` use the rule transform's `ctx` binding inside `{...}`.
167166
#[proc_macro]

‎shared/yeast-macros/src/lower.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl Rule {
5959
let __query = #query;
6060
yeast::Rule::guarded(
6161
__query,
62-
Box::new(|__ast: &yeast::Ast, __captures: &yeast::captures::Captures, __user_ctx: &mut _| {
62+
Box::new(|__ast: &yeast::Ast, __captures: &yeast::captures::Captures, __user_ctx: &_| {
6363
#(#guard_bindings)*
6464
let ast = __ast;
6565
let #ctx = __user_ctx;

‎shared/yeast/doc/yeast.md‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -513,22 +513,17 @@ rule, while a capture marked `@@` would remain raw in the transform as well.
513513
In this example `label` and `rest` are empty whenever the guard succeeds, so
514514
there is nothing to translate for those captures.
515515

516-
Guards receive the mutable user context as `ctx` and the raw AST as `ast`.
517-
The framework clones the user context before evaluating each guard. If the
518-
guard succeeds, its context mutations are visible to the rule transform and
519-
recursive translation; if it fails, the clone is discarded before the next
520-
rule is tried:
516+
Guards receive the user context as read-only `ctx` and the raw AST as `ast`.
517+
The transform receives a private clone of the context after the guard succeeds:
521518

522519
```rust
523520
rule!(
524521
(tupleExpr elements: _* @elements)
525-
where {
526-
ctx.in_pattern = true;
527-
elements
522+
where ctx.in_pattern
523+
&& elements
528524
.first()
529525
.and_then(|element| ast.get_node(*element))
530526
.is_some()
531-
}
532527
=>
533528
(tuple_pattern element: {elements})
534529
)

‎shared/yeast/src/lib.rs‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,10 +1179,9 @@ pub type Transform<C = ()> = Box<
11791179

11801180
/// Predicate evaluated after a rule's query matches and before its transform
11811181
/// runs. Returning `false` makes the rule behave as if its query did not
1182-
/// match, so the driver continues to the next rule. The user context is a
1183-
/// private clone for this rule attempt; mutations are retained for the
1184-
/// transform when the guard succeeds and discarded when it fails.
1185-
pub type Guard<C = ()> = Box<dyn Fn(&Ast, &Captures, &mut C) -> Result<bool, String> + Send + Sync>;
1182+
/// match, so the driver continues to the next rule. Guards have read-only
1183+
/// access to the shared user context.
1184+
pub type Guard<C = ()> = Box<dyn Fn(&Ast, &Captures, &C) -> Result<bool, String> + Send + Sync>;
11861185

11871186
pub struct Rule<C = ()> {
11881187
query: QueryNode,
@@ -1202,8 +1201,8 @@ impl<C> Rule<C> {
12021201
}
12031202
}
12041203

1205-
/// Construct a guarded rule. The guard sees raw captures and shares its
1206-
/// private user-context clone with the transform when it succeeds.
1204+
/// Construct a guarded rule. The guard sees raw captures and has read-only
1205+
/// access to the user context.
12071206
pub fn guarded(query: QueryNode, guard: Guard<C>, transform: Transform<C>) -> Self {
12081207
Self {
12091208
query,
@@ -1230,12 +1229,7 @@ impl<C> Rule<C> {
12301229

12311230
/// Evaluate this rule's guard against a successful query match. An
12321231
/// unguarded rule always succeeds.
1233-
fn guard_matches(
1234-
&self,
1235-
ast: &Ast,
1236-
captures: &Captures,
1237-
user_ctx: &mut C,
1238-
) -> Result<bool, String> {
1232+
fn guard_matches(&self, ast: &Ast, captures: &Captures, user_ctx: &C) -> Result<bool, String> {
12391233
if let Some(guard) = &self.guard {
12401234
guard(ast, captures, user_ctx)
12411235
} else {
@@ -1327,15 +1321,14 @@ fn apply_rules_inner<C: Clone>(
13271321
continue;
13281322
};
13291323

1330-
// Clone only after the query matches, but before the guard runs.
1331-
// Guard mutations are visible to the transform and transitively-
1332-
// translated captures when the guard succeeds; a failed guard drops
1333-
// the clone before trying the next rule.
1334-
let mut local = user_ctx.clone();
1335-
if !rule.guard_matches(ast, &captures, &mut local)? {
1324+
if !rule.guard_matches(ast, &captures, user_ctx)? {
13361325
continue;
13371326
}
13381327

1328+
// The transform gets a private context clone. Its mutations are visible
1329+
// to transitively translated captures, but not to sibling rule attempts.
1330+
let mut local = user_ctx.clone();
1331+
13391332
// Build the translator handle the transform will use to recursively
13401333
// translate captured input nodes when they are consumed.
13411334
let translator = TranslatorHandle {

‎shared/yeast/tests/test.rs‎

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -933,20 +933,16 @@ fn test_desugar_for_loop() {
933933
#[derive(Clone, Default)]
934934
struct GuardTestContext {
935935
enabled: bool,
936-
selected: bool,
937936
}
938937

939938
fn guarded_integer_rules() -> Vec<Rule<GuardTestContext>> {
940939
vec![
941940
yeast::rule!(
942941
(integer) @@raw
943-
where {
944-
ctx.selected = ctx.enabled && ast.source_text(raw) == "1";
945-
ctx.selected
946-
}
942+
where ctx.enabled && ast.source_text(raw) == "1"
947943
=>
948944
identifier {
949-
assert!(ctx.selected);
945+
assert!(ctx.enabled);
950946
tree!((identifier "guarded"))
951947
}
952948
),
@@ -966,10 +962,7 @@ fn test_rule_guard_reads_raw_capture_and_user_context() {
966962
with_passthrough_rules(guarded_integer_rules()),
967963
)];
968964
let runner = Runner::with_schema(lang, &schema, &phases);
969-
let mut user_ctx = GuardTestContext {
970-
enabled,
971-
selected: false,
972-
};
965+
let mut user_ctx = GuardTestContext { enabled };
973966
let ast = runner.run_with_ctx(input, &mut user_ctx).unwrap();
974967
dump_ast(&ast, ast.get_root(), input)
975968
}
@@ -1191,7 +1184,7 @@ fn test_guard_runs_before_capture_translation() {
11911184
}
11921185

11931186
#[test]
1194-
fn test_guard_context_mutation_is_visible_to_transform() {
1187+
fn test_guard_context_is_visible_to_transform() {
11951188
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();
11961189
let schema =
11971190
yeast::node_types_yaml::schema_from_yaml_with_language(OUTPUT_SCHEMA_YAML, &lang).unwrap();
@@ -1203,20 +1196,17 @@ fn test_guard_context_mutation_is_visible_to_transform() {
12031196
),
12041197
yeast::rule!(
12051198
(integer)
1206-
where {
1207-
ctx.selected = true;
1208-
true
1209-
}
1199+
where ctx.enabled
12101200
=>
12111201
identifier {
1212-
assert!(ctx.selected);
1202+
assert!(ctx.enabled);
12131203
tree!((identifier "guarded"))
12141204
}
12151205
),
12161206
];
12171207
let phases = vec![Phase::new("translate", rules)];
12181208
let runner = Runner::with_schema(lang, &schema, &phases);
1219-
let mut user_ctx = GuardTestContext::default();
1209+
let mut user_ctx = GuardTestContext { enabled: true };
12201210

12211211
let input = "1";
12221212
let ast = runner.run_with_ctx(input, &mut user_ctx).unwrap();

0 commit comments

Comments
 (0)