@@ -3,8 +3,10 @@ use aether_syntax::AnyRExpression;
33use aether_syntax:: AnyRValue ;
44use aether_syntax:: RArgument ;
55use aether_syntax:: RCall ;
6+ use biome_rowan:: AstNode ;
67use biome_rowan:: AstPtr ;
78use biome_rowan:: AstSeparatedList ;
9+ use biome_rowan:: WalkEvent ;
810// Re-exported so consumers building an `AssignBinding` (custom `EffectHandler`s)
911// can name the `name_expr` field's type without depending on oak_core directly.
1012pub use oak_core:: range:: RangedAstPtr ;
@@ -140,6 +142,16 @@ impl CallContext {
140142 _ => None ,
141143 }
142144 }
145+
146+ /// Statically evaluate an argument's value expression to a bool.
147+ pub fn resolve_static_bool ( & self , value : & AnyRExpression ) -> Option < bool > {
148+ match value {
149+ AnyRExpression :: RTrueExpression ( _) => Some ( true ) ,
150+ AnyRExpression :: RFalseExpression ( _) => Some ( false ) ,
151+ // Static resolution of expressions is not implemented yet
152+ _ => None ,
153+ }
154+ }
143155}
144156
145157/// A formal a handler wants to locate in a call, by name and by its position in
@@ -155,9 +167,19 @@ pub struct Formal {
155167}
156168
157169/// A call's resolved argument effects: for each argument in call order, the
158- /// annotated argument it matched, or `None` for a plain (standard-eval)
159- /// argument.
160- pub type ResolvedArgumentEffects = Vec < Option < & ' static Argument > > ;
170+ /// effect it resolved to, or `None` for a plain (standard-eval) argument.
171+ pub type ResolvedArgumentEffects = Vec < Option < ResolvedArgumentEffect > > ;
172+
173+ /// The resolved, per-call effect of one argument. The builder consumes these.
174+ #[ derive( Debug , Clone ) ]
175+ pub enum ResolvedArgumentEffect {
176+ /// Quote plus Eval in a controlled scope, fused.
177+ Nse { scope : NseScope , timing : NseTiming } ,
178+ /// Captured unevaluated. `holes` are the sub-expressions that escape back to
179+ /// evaluation (e.g. bquote's `.()` contents), walked normally; everything
180+ /// else in the argument is inert. Empty for a plain `quote()`.
181+ Quote { holes : Vec < AnyRExpression > } ,
182+ }
161183
162184/// Declares how a function evaluates its annotated arguments, and serves as the
163185/// default [`EffectHandler`] for it by matching the declaration to a call.
@@ -175,18 +197,27 @@ pub struct Argument {
175197}
176198
177199/// What static operation an argument's evaluation calls for, mirroring R's
178- /// evaluation model. Absence (an argument not listed on an annotation) means
179- /// standard evaluation of an unquoted expression, the common case.
200+ /// evaluation model.
180201#[ derive( Debug , Clone , Copy ) ]
181202pub enum ArgumentEffect {
182203 /// Quote plus Eval in a controlled scope, fused
183204 Nse { scope : NseScope , timing : NseTiming } ,
184205 /// Captured unevaluated, so its symbols are not uses and nothing in it runs.
185- /// `quote`, `bquote`. TODO:`bquote(.(foo))` should unquote `foo`. This
186- /// requires implementing the `Eval` effect.
206+ /// `quote`. A function that unquotes (`bquote()`, whose `.()` holes escape)
207+ /// can't be expressed statically, and must use a custom handler instead of
208+ /// this variant.
187209 Quote ,
188210}
189211
212+ impl ArgumentEffect {
213+ fn resolve ( self ) -> ResolvedArgumentEffect {
214+ match self {
215+ ArgumentEffect :: Nse { scope, timing } => ResolvedArgumentEffect :: Nse { scope, timing } ,
216+ ArgumentEffect :: Quote => ResolvedArgumentEffect :: Quote { holes : Vec :: new ( ) } ,
217+ }
218+ }
219+ }
220+
190221impl EffectHandler for ArgumentsAnnotation {
191222 type Output = ResolvedArgumentEffects ;
192223
@@ -205,12 +236,113 @@ impl EffectHandler for ArgumentsAnnotation {
205236 Some (
206237 matched
207238 . into_iter ( )
208- . map ( |formal| formal. map ( |i| & arguments[ i] ) )
239+ . map ( |formal| formal. map ( |i| arguments[ i] . effect . resolve ( ) ) )
209240 . collect ( ) ,
210241 )
211242 }
212243}
213244
245+ /// Handler for `bquote()`. It quotes its `expr` argument like `quote()`, but a
246+ /// `.(X)` inside escapes back to evaluation, so `X` is a live sub-expression.
247+ /// Recognizing `.()` is specific to bquote, so it lives in this handler rather
248+ /// than in the shared [`ArgumentEffect`] vocabulary.
249+ #[ derive( Debug , Clone , Copy ) ]
250+ pub struct BquoteHandler ;
251+
252+ impl EffectHandler for BquoteHandler {
253+ type Output = ResolvedArgumentEffects ;
254+
255+ fn resolve ( & self , call : & RCall , ctx : & CallContext ) -> Option < ResolvedArgumentEffects > {
256+ // `bquote(expr, where, splice)`: only `expr` (the first positional) is
257+ // quoted. The other arguments are ordinary values.
258+ let formals = [
259+ Formal {
260+ name : "expr" ,
261+ position : 0 ,
262+ } ,
263+ Formal {
264+ name : "splice" ,
265+ position : 2 ,
266+ } ,
267+ ] ;
268+ let matched = ctx. match_arguments ( call, & formals) ;
269+
270+ let args = call. arguments ( ) . ok ( ) ?;
271+ let values: Vec < Option < AnyRExpression > > = args
272+ . items ( )
273+ . iter ( )
274+ . map ( |item| item. ok ( ) . and_then ( |arg| arg. value ( ) ) )
275+ . collect ( ) ;
276+
277+ // `..()` only splices under `splice = TRUE`.
278+ // TODO: resolve a dynamic `splice` argument.
279+ let splice = matched
280+ . iter ( )
281+ . position ( |formal| * formal == Some ( 1 ) )
282+ . and_then ( |i| values. get ( i) )
283+ . and_then ( |value| value. as_ref ( ) )
284+ . and_then ( |value| ctx. resolve_static_bool ( value) )
285+ . unwrap_or ( false ) ;
286+
287+ Some (
288+ matched
289+ . into_iter ( )
290+ . enumerate ( )
291+ . map ( |( i, formal) | {
292+ // Only `expr` (formal 0) is quoted
293+ if formal != Some ( 0 ) {
294+ return None ;
295+ }
296+ let holes = values
297+ . get ( i)
298+ . and_then ( |value| value. as_ref ( ) )
299+ . map ( |expr| unquote_holes ( expr, splice) )
300+ . unwrap_or_default ( ) ;
301+ Some ( ResolvedArgumentEffect :: Quote { holes } )
302+ } )
303+ . collect ( ) ,
304+ )
305+ }
306+ }
307+
308+ /// The unquote holes inside a bquote-quoted expression: the escaped argument of
309+ /// each `.(foo)` call, plus each `..(foo)` when `splice` is on.
310+ fn unquote_holes ( expr : & AnyRExpression , splice : bool ) -> Vec < AnyRExpression > {
311+ let mut holes = Vec :: new ( ) ;
312+ let mut preorder = expr. syntax ( ) . preorder ( ) ;
313+ while let Some ( event) = preorder. next ( ) {
314+ let WalkEvent :: Enter ( node) = event else {
315+ continue ;
316+ } ;
317+ let Some ( call) = RCall :: cast ( node) else {
318+ continue ;
319+ } ;
320+ if let Some ( hole) = unquote_hole ( & call, splice) {
321+ holes. push ( hole) ;
322+ preorder. skip_subtree ( ) ;
323+ }
324+ }
325+ holes
326+ }
327+
328+ /// The escaped expression of a `.(foo)` unquote call, or a `..(foo)` splice
329+ /// unquote when `splice` is on, or `None` when `call` isn't one. bquote's
330+ /// unquote operator is the function `.`, and its splice unquote is `..`.
331+ fn unquote_hole ( call : & RCall , splice : bool ) -> Option < AnyRExpression > {
332+ let AnyRExpression :: RIdentifier ( func) = call. function ( ) . ok ( ) ? else {
333+ return None ;
334+ } ;
335+ let is_unquote = match func. name_text ( ) . as_str ( ) {
336+ "." => true ,
337+ ".." => splice,
338+ _ => false ,
339+ } ;
340+ if !is_unquote {
341+ return None ;
342+ }
343+ call. arguments ( ) . ok ( ) ?. items ( ) . iter ( ) . next ( ) ?. ok ( ) ?. value ( )
344+ }
345+
214346/// Declares how an attach function (`library()`, `require()`) names its package,
215347/// and serves as the default [`EffectHandler`] for it by extracting that package
216348/// from a call.
0 commit comments