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
37 changes: 2 additions & 35 deletions crates/oak_semantic/src/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ pub trait EffectHandler: std::fmt::Debug + Sync {
/// Resolve this effect for `call`, or `None` when the call isn't in a shape
/// this handler recognizes.
///
/// `ctx` resolves information the call's own syntax doesn't carry, e.g. what
/// a `character.only = TRUE` variable is bound to. Unused until that lands.
/// `ctx` provides semantic resolution, e.g. resolve an argument to a
/// statically known string or boolean.
fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<Self::Output>;
}

Expand Down Expand Up @@ -304,39 +304,6 @@ impl EffectHandler for ArgumentsAnnotation {
}
}

/// Declares how an attach function (`library()`, `require()`) names its package,
/// and serves as the default [`EffectHandler`] for it by extracting that package
/// from a call.
#[derive(Debug, Clone, Copy)]
pub struct AttachAnnotation {
/// Whether the callee has a `character.only`-style flag. Unread today.
pub character_only: bool,
}

impl EffectHandler for AttachAnnotation {
type Output = String;

fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<String> {
// `library()`/`require()` name their package in the `package` formal,
// the first positional argument.
let formals = [Formal {
name: "package",
position: 0,
}];
let matched = ctx.match_arguments(call, &formals);

let arg_index = matched.iter().position(|formal| *formal == Some(0))?;
let arg = call.arguments().ok()?.items().iter().nth(arg_index)?.ok()?;
let value = arg.value()?;

match &value {
AnyRExpression::RIdentifier(ident) => Some(ident.name_text()),
AnyRExpression::AnyRValue(AnyRValue::RStringValue(s)) => s.string_text(),
_ => None,
}
}
}

/// Declares how a source function (`source()`) names the file it reads, and
/// serves as the default [`EffectHandler`] for it by pulling that path out of a
/// call.
Expand Down
19 changes: 0 additions & 19 deletions crates/oak_semantic/src/effects/contrib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,6 @@ macro_rules! quoted {
}
pub(crate) use quoted;

/// An attach entry: `(package-argument position, has-`character.only`-flag)`.
macro_rules! attach {
($pkg:literal, $func:literal, $pos:literal, $character_only:literal) => {
$crate::effects::contrib::Entry {
package: $pkg,
function: $func,
effects: $crate::effects::EffectsHandlers {
arguments: None,
attach: Some(&$crate::effects::AttachAnnotation {
character_only: $character_only,
}),
source: None,
assign: None,
},
}
};
}
pub(crate) use attach;

/// A source entry: `(path-argument position)`. The function reads and evaluates
/// another file, injecting its top-level names into the caller.
macro_rules! source {
Expand Down
77 changes: 73 additions & 4 deletions crates/oak_semantic/src/effects/contrib/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use biome_rowan::WalkEvent;
use oak_core::syntax_ext::RIdentifierExt;

use crate::effects::contrib::assign;
use crate::effects::contrib::attach;
use crate::effects::contrib::nse;
use crate::effects::contrib::quoted;
use crate::effects::contrib::source;
Expand Down Expand Up @@ -43,9 +42,9 @@ pub(crate) static ENTRIES: &[Entry] = &[
assign: None,
},
},
// base attach
attach!("base", "library", 0, true),
attach!("base", "require", 0, true),
// base attach. `library`/`require` share `LibraryHandler` (below).
attach_entry("library"),
attach_entry("require"),
// base source
source!("base", "source", 0),
// base assign
Expand Down Expand Up @@ -152,3 +151,73 @@ fn unquote_hole(call: &RCall, splice: bool) -> Option<AnyRExpression> {
}
call.arguments().ok()?.items().iter().next()?.ok()?.value()
}

/// Build the attach [`Entry`] for a base function served by [`LibraryHandler`].
const fn attach_entry(function: &'static str) -> Entry {
Entry {
package: "base",
function,
effects: EffectsHandlers {
arguments: None,
attach: Some(&LibraryHandler),
source: None,
assign: None,
},
}
}

/// Handler for `library()` and `require()`. Names the attached package from the
/// first argument, read as quoted (the symbol or string as written, so
/// `library(dplyr)` attaches `dplyr`). `character.only = TRUE` flips that
/// argument to standard eval (a value to resolve, `library(pkg, character.only =
/// TRUE)`), matching R. That flag is specific to these callees, so it lives in
/// this handler rather than the shared attach vocabulary.
#[derive(Debug, Clone, Copy)]
pub(crate) struct LibraryHandler;

impl EffectHandler for LibraryHandler {
type Output = String;

fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<String> {
// `character.only` sits at signature position 4 in both callees; in
// practice it's passed by name.
let formals = [
Formal {
name: "package",
position: 0,
},
Formal {
name: "character.only",
position: 4,
},
];
let matched = ctx.match_arguments(call, &formals);

let args = call.arguments().ok()?;
let values: Vec<Option<AnyRExpression>> = args
.items()
.iter()
.map(|item| item.ok().and_then(|arg| arg.value()))
.collect();

let package = matched
.iter()
.position(|formal| *formal == Some(0))
.and_then(|i| values.get(i))
.and_then(|value| value.as_ref())?;

let character_only = matched
.iter()
.position(|formal| *formal == Some(1))
.and_then(|i| values.get(i))
.and_then(|value| value.as_ref())
.and_then(|value| ctx.resolve_static_bool(value))
.unwrap_or(false);

if character_only {
ctx.resolve_static_string(package)
} else {
ctx.resolve_quoted_symbol_or_string(package)
}
}
}
29 changes: 29 additions & 0 deletions crates/oak_semantic/tests/integration/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,35 @@ fn test_directive_multiple_arguments() {
}]);
}

#[test]
fn test_directive_character_only_string() {
// `character.only = TRUE` reads the package argument with the standard rule.
// A string literal resolves to its text.
let index = index_with_base("library(\"dplyr\", character.only = TRUE)");
assert_eq!(semantic_call_kinds(&index), [&SemanticCallKind::Attach {
package: "dplyr".into()
}]);
}

#[test]
fn test_directive_character_only_identifier_not_attached() {
// With `character.only = TRUE` the package argument is a variable to resolve,
// not a symbol. We can't chase it statically, so nothing is attached, rather
// than wrongly attaching a package literally named `x`.
let index = index_with_base("library(x, character.only = TRUE)");
assert_eq!(semantic_call_kinds(&index), Vec::<&SemanticCallKind>::new());
}

#[test]
fn test_directive_character_only_false_is_quoted() {
// `character.only = FALSE` leaves the package argument quoted, so the symbol
// text is the package name.
let index = index_with_base("library(dplyr, character.only = FALSE)");
assert_eq!(semantic_call_kinds(&index), [&SemanticCallKind::Attach {
package: "dplyr".into()
}]);
}

#[test]
fn test_directive_no_arguments_ignored() {
let index = index_with_base("library()");
Expand Down
Loading