Skip to content

Commit f2eeae6

Browse files
committed
Streamline dispatch of Assign operators
1 parent dacdce8 commit f2eeae6

4 files changed

Lines changed: 107 additions & 37 deletions

File tree

crates/oak_semantic/src/builder/builder_nse.rs

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ use aether_syntax::RCall;
44
use aether_syntax::RSyntaxKind;
55
use biome_rowan::AstNode;
66
use biome_rowan::AstNodeList;
7-
use biome_rowan::AstPtr;
87
use biome_rowan::AstSeparatedList;
98
use biome_rowan::TextRange;
10-
use oak_core::range::RangedAstPtr;
119
use oak_core::syntax_ext::AnyRSelectorExt;
1210
use oak_core::syntax_ext::RIdentifierExt;
1311

@@ -20,6 +18,7 @@ use super::SemanticIndexBuilder;
2018
use super::SourcedFile;
2119
use crate::effects::AssignBinding;
2220
use crate::effects::CallContext;
21+
use crate::effects::EffectSite;
2322
use crate::effects::Effects;
2423
use crate::effects::EffectsHandlers;
2524
use crate::effects::ResolvedArgumentEffect;
@@ -179,21 +178,24 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
179178

180179
/// Scan a binary operator for an assign effect (e.g. magrittr's `x %<>% f()`)
181180
pub(super) fn scan_operator_assign(&mut self, bin: &RBinaryExpression) {
182-
let Some(binding) = self.resolve_operator_assign(bin) else {
181+
let Some(bindings) = self.resolve_operator_assign(bin) else {
183182
return;
184183
};
185-
self.record_binding(binding.name.clone());
186-
self.call_resolutions
187-
.entry(bin.syntax().text_trimmed_range())
188-
.or_default()
189-
.assign
190-
.push(binding);
184+
let range = bin.syntax().text_trimmed_range();
185+
for binding in bindings {
186+
self.record_binding(binding.name.clone());
187+
self.call_resolutions
188+
.entry(range)
189+
.or_default()
190+
.assign
191+
.push(binding);
192+
}
191193
}
192194

193195
/// Recognize a binding operator (`x %<>% f()`, `x %<~% expr`, `x := expr`)
194-
/// as an assign effect and build its binding, or `None` for any other binary
196+
/// as an assign effect and build its bindings, or `None` for any other binary
195197
/// operator.
196-
fn resolve_operator_assign(&mut self, bin: &RBinaryExpression) -> Option<AssignBinding> {
198+
fn resolve_operator_assign(&mut self, bin: &RBinaryExpression) -> Option<Vec<AssignBinding>> {
197199
let op = bin.operator().ok()?;
198200

199201
// A binding operator is either a `%...%` (`SPECIAL`, e.g. `%<>%`, where
@@ -211,17 +213,8 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
211213
}
212214

213215
let handlers = self.resolve_symbol_effects(op_text, bin.syntax().text_trimmed_range())?;
214-
let _assign = handlers.assign?;
215-
216-
let left = bin.left().ok()?;
217-
let right = bin.right().ok()?;
218-
let (name, _) = assignment_name(&left)?;
219-
220-
Some(AssignBinding {
221-
name,
222-
name_expr: RangedAstPtr::new(&left),
223-
value_expr: Some(AstPtr::new(&right)),
224-
})
216+
let ctx = CallContext::new();
217+
handlers.assign?.resolve(EffectSite::Operator(bin), &ctx)
225218
}
226219

227220
/// Copy the names a `Current + Lazy` body defines into the owner's
@@ -333,7 +326,7 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
333326
.and_then(|handler| handler.resolve(call, &ctx));
334327
let assign = handlers
335328
.assign
336-
.and_then(|handler| handler.resolve(call, &ctx));
329+
.and_then(|handler| handler.resolve(EffectSite::Call(call), &ctx));
337330

338331
Some(Effects {
339332
arguments,

crates/oak_semantic/src/effects.rs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ use aether_syntax::AnyRArgumentName;
22
use aether_syntax::AnyRExpression;
33
use aether_syntax::AnyRValue;
44
use aether_syntax::RArgument;
5+
use aether_syntax::RBinaryExpression;
56
use aether_syntax::RCall;
67
use biome_rowan::AstNode;
78
use biome_rowan::AstPtr;
89
use biome_rowan::AstSeparatedList;
910
use biome_rowan::WalkEvent;
10-
// Re-exported so consumers building an `AssignBinding` (custom `EffectHandler`s)
11+
// Re-exported so consumers building an `AssignBinding` (custom `AssignHandler`s)
1112
// can name the `name_expr` field's type without depending on oak_core directly.
1213
pub use oak_core::range::RangedAstPtr;
1314
use oak_core::syntax_ext::RIdentifierExt;
@@ -53,7 +54,7 @@ pub struct EffectsHandlers {
5354
pub arguments: Option<&'static dyn EffectHandler<Output = ResolvedArgumentEffects>>,
5455
pub attach: Option<&'static dyn EffectHandler<Output = String>>,
5556
pub source: Option<&'static dyn EffectHandler<Output = Vec<String>>>,
56-
pub assign: Option<&'static dyn EffectHandler<Output = Vec<AssignBinding>>>,
57+
pub assign: Option<&'static dyn AssignHandler>,
5758
}
5859

5960
/// Resolver for an effect of a call.
@@ -73,6 +74,25 @@ pub trait EffectHandler: std::fmt::Debug + Sync {
7374
fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<Self::Output>;
7475
}
7576

77+
/// Where an effect is invoked. Most effects are only ever calls but an Assign
78+
/// effect can also be a binding operator (`x %<>% f`). [`AssignHandler`] takes
79+
/// this to disambiguate rather than a bare call.
80+
pub enum EffectSite<'a> {
81+
Call(&'a RCall),
82+
Operator(&'a RBinaryExpression),
83+
}
84+
85+
/// Resolver for an assign-like effect.
86+
///
87+
/// Separate from [`EffectHandler`] because an assign has two invocation shapes,
88+
/// a call (`assign("x", v)`) and a binding operator (`x %<>% f`).
89+
///
90+
/// Contributed statically like [`EffectHandler`], so it's `Sync` for the
91+
/// registry `static`s.
92+
pub trait AssignHandler: std::fmt::Debug + Sync {
93+
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>>;
94+
}
95+
7696
/// Context for effect handlers.
7797
///
7898
/// Allows querying the properties or static values of arguments. Stateless
@@ -143,6 +163,15 @@ impl CallContext {
143163
}
144164
}
145165

166+
/// Read a quoted name argument. E.g. the LHS of an Assign operator.
167+
pub fn resolve_quoted_symbol_or_string(&self, value: &AnyRExpression) -> Option<String> {
168+
match value {
169+
AnyRExpression::RIdentifier(ident) => Some(ident.name_text()),
170+
AnyRExpression::AnyRValue(AnyRValue::RStringValue(s)) => s.string_text(),
171+
_ => None,
172+
}
173+
}
174+
146175
/// Statically evaluate an argument's value expression to a bool.
147176
pub fn resolve_static_bool(&self, value: &AnyRExpression) -> Option<bool> {
148177
match value {
@@ -454,10 +483,11 @@ pub struct AssignAnnotation {
454483
pub position: usize,
455484
}
456485

457-
impl EffectHandler for AssignAnnotation {
458-
type Output = Vec<AssignBinding>;
459-
460-
fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<Vec<AssignBinding>> {
486+
impl AssignHandler for AssignAnnotation {
487+
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>> {
488+
let EffectSite::Call(call) = site else {
489+
return None;
490+
};
461491
let args = call.arguments().ok()?;
462492

463493
// Matched positionally among unnamed arguments, same as `source`, so a
@@ -512,6 +542,30 @@ impl EffectHandler for AssignAnnotation {
512542
}
513543
}
514544

545+
/// Handler for a binding operator (`x %<>% f()`, `x %<~% expr`, `x := expr`).
546+
///
547+
/// The operator captures its LHS unevaluated.
548+
#[derive(Debug, Clone, Copy)]
549+
pub struct BindingOperatorHandler;
550+
551+
impl AssignHandler for BindingOperatorHandler {
552+
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>> {
553+
let EffectSite::Operator(bin) = site else {
554+
return None;
555+
};
556+
let left = bin.left().ok()?;
557+
let right = bin.right().ok()?;
558+
559+
let name = ctx.resolve_quoted_symbol_or_string(&left)?;
560+
561+
Some(vec![AssignBinding {
562+
name,
563+
name_expr: RangedAstPtr::new(&left),
564+
value_expr: Some(AstPtr::new(&right)),
565+
}])
566+
}
567+
}
568+
515569
/// Match a named argument against `formals`. Returns the index of the matched
516570
/// formal.
517571
///

crates/oak_semantic/src/effects_registry.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::effects::ArgumentEffect;
33
use crate::effects::ArgumentsAnnotation;
44
use crate::effects::AssignAnnotation;
55
use crate::effects::AttachAnnotation;
6+
use crate::effects::BindingOperatorHandler;
67
use crate::effects::BquoteHandler;
78
use crate::effects::EffectsHandlers;
89
use crate::effects::SourceAnnotation;
@@ -121,7 +122,8 @@ macro_rules! source {
121122
}
122123

123124
/// An assign entry: `(name-argument position)`. The function binds a name in the
124-
/// current scope.
125+
/// current scope, naming it in a positional argument it evaluates (`assign("x",
126+
/// v)`).
125127
macro_rules! assign {
126128
($pkg:literal, $func:literal, $pos:literal) => {
127129
Entry {
@@ -137,6 +139,24 @@ macro_rules! assign {
137139
};
138140
}
139141

142+
/// An assign-operator entry: a binding operator (`x %<>% f`, `x := v`) that binds
143+
/// a name in the current scope. It captures its LHS unevaluated, so the name
144+
/// comes from the LHS text rather than a positional argument, hence no position.
145+
macro_rules! assign_op {
146+
($pkg:literal, $func:literal) => {
147+
Entry {
148+
package: $pkg,
149+
function: $func,
150+
effects: EffectsHandlers {
151+
arguments: None,
152+
attach: None,
153+
source: None,
154+
assign: Some(&BindingOperatorHandler),
155+
},
156+
}
157+
};
158+
}
159+
140160
static REGISTRY: &[Entry] = &[
141161
// base NSE
142162
nse!("base", "evalq", ("expr", 0, Current, Eager)),
@@ -168,9 +188,9 @@ static REGISTRY: &[Entry] = &[
168188
assign!("base", "assign", 0),
169189
assign!("base", "delayedAssign", 0),
170190
// magrittr / rlang / S7 binding operators
171-
assign!("magrittr", "%<>%", 0),
172-
assign!("rlang", "%<~%", 0),
173-
assign!("S7", ":=", 0),
191+
assign_op!("magrittr", "%<>%"),
192+
assign_op!("rlang", "%<~%"),
193+
assign_op!("S7", ":="),
174194
// rlang
175195
nse!("rlang", "on_load", ("expr", 0, Current, Lazy)),
176196
// shiny

crates/oak_semantic/tests/integration/builder.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use biome_rowan::AstNode;
66
use biome_rowan::AstSeparatedList;
77
use oak_semantic::build_index;
88
use oak_semantic::effects::AssignBinding;
9+
use oak_semantic::effects::AssignHandler;
910
use oak_semantic::effects::CallContext;
1011
use oak_semantic::effects::EffectHandler;
12+
use oak_semantic::effects::EffectSite;
1113
use oak_semantic::effects::RangedAstPtr;
1214
use oak_semantic::effects::SourceAnnotation;
1315
use oak_semantic::effects_registry;
@@ -2329,10 +2331,11 @@ struct MultiAssignHandler;
23292331

23302332
static MULTI_ASSIGN_HANDLER: MultiAssignHandler = MultiAssignHandler;
23312333

2332-
impl EffectHandler for MultiAssignHandler {
2333-
type Output = Vec<AssignBinding>;
2334-
2335-
fn resolve(&self, call: &RCall, _ctx: &CallContext) -> Option<Vec<AssignBinding>> {
2334+
impl AssignHandler for MultiAssignHandler {
2335+
fn resolve(&self, site: EffectSite, _ctx: &CallContext) -> Option<Vec<AssignBinding>> {
2336+
let EffectSite::Call(call) = site else {
2337+
return None;
2338+
};
23362339
// Point every binding's handles at the first argument. This test only
23372340
// checks that multiple defs are created and resolve, not their ranges.
23382341
let expr = call

0 commit comments

Comments
 (0)