Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol for extending the variables pane #560

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
97ee312
Add methods registration and dispatching
dfalbel Sep 14, 2024
cc1285b
Dispatch on methods before relying on defaults
dfalbel Sep 14, 2024
7926192
add more supported methods
dfalbel Sep 14, 2024
90ee3ab
Use a strum enum to make less error prone method dispatching
dfalbel Sep 14, 2024
501b343
Favor let Some(x) instead of match
dfalbel Sep 14, 2024
6d4f7e8
use let Ok() instead
dfalbel Sep 14, 2024
ba1c37d
Move into an R based impl
dfalbel Sep 16, 2024
94a4db2
minor revision
t-kalinowski Sep 17, 2024
96783ba
typo
t-kalinowski Sep 17, 2024
2856256
Make a method instead
dfalbel Sep 17, 2024
1390024
Add some unit tests
dfalbel Sep 18, 2024
891e9e4
Add some documentation
dfalbel Sep 24, 2024
5ac78f0
Revise 'Extending Variables Pane` doc
t-kalinowski Sep 25, 2024
92e8c30
Additional note
t-kalinowski Sep 25, 2024
d3210b8
fix tests
dfalbel Oct 2, 2024
e85af96
Find namespaced method calls like `foo:::bar`
t-kalinowski Oct 2, 2024
a2f876c
Remove has_ark_method
dfalbel Oct 2, 2024
8676851
use `r_task`
dfalbel Oct 2, 2024
8551bf3
Use `if let Err` construct
dfalbel Oct 9, 2024
d3565fe
Use `is_string` helper
dfalbel Oct 9, 2024
c889216
USe `cls` for consistency
dfalbel Oct 9, 2024
63976ad
Improve readability
dfalbel Oct 9, 2024
9ba6a7e
Simplify function
dfalbel Oct 9, 2024
648b76c
Simplify note
dfalbel Oct 9, 2024
16d4ab8
Use `&self` instead
dfalbel Oct 9, 2024
aadef2b
Use `RCall` instead
dfalbel Oct 9, 2024
51111c0
Use `RArgument`
dfalbel Oct 9, 2024
27b7f02
Add note on why we store a call
dfalbel Oct 9, 2024
a21dc37
Use different namespacing. `.ps` -> `.ark` and `ark_variables` -> `po…
dfalbel Oct 9, 2024
ff887ff
Move `methods.rs` to it's own module one level above.
dfalbel Oct 9, 2024
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
26 changes: 23 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ yaml-rust = "0.4.5"
winsafe = { version = "0.0.19", features = ["kernel"] }
struct-field-names-as-array = "0.3.0"
strum = "0.26.2"
strum_macros = "0.26.2"
futures = "0.3.30"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
Expand Down
9 changes: 9 additions & 0 deletions crates/ark/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ use crate::srcref::ns_populate_srcref;
use crate::srcref::resource_loaded_namespaces;
use crate::startup;
use crate::sys::console::console_to_utf8;
use crate::variables::methods::populate_methods_from_loaded_namespaces;
use crate::variables::methods::populate_variable_methods_table;

/// An enum representing the different modes in which the R session can run.
#[derive(PartialEq, Clone)]
Expand Down Expand Up @@ -403,6 +405,9 @@ impl RMain {
}
}

populate_methods_from_loaded_namespaces()
.or_log_error("Can't populate variables pane methods from loaded packages");
dfalbel marked this conversation as resolved.
Show resolved Hide resolved

// Set up the global error handler (after support function initialization)
errors::initialize();

Expand Down Expand Up @@ -1720,6 +1725,10 @@ unsafe extern "C" fn ps_onload_hook(pkg: SEXP, _path: SEXP) -> anyhow::Result<SE
// Need to reset parent as this might run in the context of another thread's R task
let _span = tracing::trace_span!(parent: None, "onload_hook", pkg = pkg).entered();

// Populate variables pane methods
populate_variable_methods_table(pkg.as_str())
.or_log_error("Failed populating variables pane methods");

// Populate fake source refs if needed
if do_resource_namespaces() {
r_task::spawn_idle(|| async move {
Expand Down
56 changes: 56 additions & 0 deletions crates/ark/src/modules/positron/methods.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#
# methods.R
#
# Copyright (C) 2024 Posit Software, PBC. All rights reserved.
#
#

ark_methods_table <- new.env(parent = emptyenv())
ark_methods_table$ark_variable_display_value <- new.env(parent = emptyenv())
ark_methods_table$ark_variable_display_type <- new.env(parent = emptyenv())
ark_methods_table$ark_variable_has_children <- new.env(parent = emptyenv())
ark_methods_table$ark_variable_kind <- new.env(parent = emptyenv())
lockEnvironment(ark_methods_table, TRUE)

#' Register the methods with the Positron runtime
#'
#' @param generic Generic function name as a character to register
#' @param class Class name as a character
#' @param method A method to be registered. Should be a call object.
#' @export
.ps.register_ark_method <- function(generic, class, method) {
stopifnot(
typeof(generic) == "character",
length(generic) == 1,
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
generic %in% c(
"ark_variable_display_value",
"ark_variable_display_type",
"ark_variable_has_children",
"ark_variable_kind"
),
typeof(class) == "character"
)
for (cls in class) {
assign(cls, method, envir = ark_methods_table[[generic]])
}
invisible()
}

call_ark_method <- function(generic, object, ...) {
methods_table <- ark_methods_table[[generic]]

if (is.null(methods_table)) {
return(NULL)
}

for (cl in class(object)) {
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
if (!is.null(method <- get0(cl, envir = methods_table))) {
return(eval(
as.call(list(method, object, ...)),
envir = globalenv()
))
Comment on lines +47 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid inlining the method in the call, we could assign the function in a child of global (either as generic or generic + class) and evaluate there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's currently implemented method is not necessarily a function. It could be a call object, a symbol or a function.

}
}

NULL
}
158 changes: 158 additions & 0 deletions crates/ark/src/variables/methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// methods.rs
//
// Copyright (C) 2024 by Posit Software, PBC
//
//

use anyhow::anyhow;
use harp::environment::r_ns_env;
use harp::environment::BindingValue;
use harp::exec::RFunction;
use harp::exec::RFunctionExt;
use harp::r_null;
use harp::r_symbol;
use harp::utils::r_is_object;
use harp::RObject;
use libr::Rf_lang3;
use libr::SEXP;
use stdext::result::ResultOrLog;
use strum::IntoEnumIterator;
use strum_macros::Display;
use strum_macros::EnumIter;
use strum_macros::EnumString;
use strum_macros::IntoStaticStr;

use crate::modules::ARK_ENVS;

#[derive(Debug, PartialEq, EnumString, EnumIter, IntoStaticStr, Display, Eq, Hash, Clone)]
pub enum ArkGenerics {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be in its own module one level above?

Or renamed to ArkVariableGenerics.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a preference? It feels like this could be used elsewhere, so we could move to its own module above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved one level above in ff887ff . Happy to rever if you prefer making it specific for the variables pane.

#[strum(serialize = "ark_variable_display_value")]
VariableDisplayValue,

#[strum(serialize = "ark_variable_display_type")]
VariableDisplayType,

#[strum(serialize = "ark_variable_has_children")]
VariableHasChildren,

#[strum(serialize = "ark_variable_kind")]
VariableKind,
}

impl ArkGenerics {
// Dispatches the method on `x`
// Returns
// - `None` if no method was found,
// - `Err` if method was found and errored
// - T, if method was found and was succesfully executed
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
pub fn try_dispatch<T>(
&self,
x: SEXP,
args: Vec<(String, RObject)>,
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
) -> anyhow::Result<Option<T>>
where
// Making this a generic allows us to handle the conversion to the expected output
// type within the dispatch, which is much more ergonomic.
T: TryFrom<RObject>,
<T as TryFrom<harp::RObject>>::Error: std::fmt::Debug,
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
{
if !r_is_object(x) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is rather generic infrastructure, we might want to allow registering methods for base types in the future (only us would be allowed to)?

return Ok(None);
}

let generic: &str = self.into();
let mut call = RFunction::new("", "call_ark_method");

call.add(generic);
call.add(x);

for (name, value) in args.into_iter() {
call.param(name.as_str(), value);
}

let result = call.call_in(ARK_ENVS.positron_ns)?;

// No method for that object
if result.sexp == r_null() {
return Ok(None);
}

// Convert the result to the expected return type
match result.try_into() {
Ok(value) => Ok(Some(value)),
Err(err) => Err(anyhow!("Conversion failed: {err:?}")),
}
}

pub fn register_method(generic: Self, class: &str, method: RObject) -> anyhow::Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to take a consuming self (as opposed to &self)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning was that a static method would make more clear that we're actually modfying some global state and not just an object state. But loooks weird indeed - fixed in 16d4ab8

let generic_name: &str = generic.into();
RFunction::new("", ".ps.register_ark_method")
.add(RObject::try_from(generic_name)?)
.add(RObject::try_from(class)?)
.add(method)
.call_in(ARK_ENVS.positron_ns)?;
Ok(())
}

pub fn register_method_from_package(
generic: Self,
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
class: &str,
package: &str,
) -> anyhow::Result<()> {
let method = RObject::from(unsafe {
Rf_lang3(
r_symbol!(":::"),
r_symbol!(package),
r_symbol!(format!("{generic}.{class}")),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can replace this with RCall which works similarly to RFunction (the latter is implemented in terms of the former).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why not pass a function to register_method()? To avoid inlining objects in R calls? If so needs a comment, the implicit evaluation is surprising.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, the motivation for storing a call pkgname:::methodname instead of the closure object was to ensure that:

  1. Everything worked seamlessly with devtools::load_all() and similar functions (no risk of calling an outdated method from a stale cache);
  2. An escape hatch remained available for monkey-patching a method in the package namespace (e.g., to enable workarounds for misbehaving methods).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated to use RCall in aadef2b

});
Self::register_method(generic, class, method)?;
dfalbel marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}

// Checks if a symbol name is a method and returns it's class
fn parse_method(name: &String) -> Option<(Self, String)> {
for method in ArkGenerics::iter() {
let method_str: &str = method.clone().into();
if name.starts_with::<&str>(method_str) {
if let Some((_, class)) = name.split_once(".") {
return Some((method, class.to_string()));
}
}
}
None
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat!


pub fn populate_methods_from_loaded_namespaces() -> anyhow::Result<()> {
let loaded = RFunction::new("base", "loadedNamespaces").call()?;
let loaded: Vec<String> = loaded.try_into()?;

for pkg in loaded.into_iter() {
populate_variable_methods_table(pkg.as_str()).or_log_error("Failed populating methods");
}

Ok(())
}

pub fn populate_variable_methods_table(package: &str) -> anyhow::Result<()> {
let ns = r_ns_env(package)?;
let symbol_names = ns
.iter()
.filter_map(Result::ok)
.filter(|b| match b.value {
BindingValue::Standard { .. } => true,
BindingValue::Promise { .. } => true,
_ => false,
})
.map(|b| -> String { b.name.into() });

for name in symbol_names {
if let Some((generic, class)) = ArkGenerics::parse_method(&name) {
ArkGenerics::register_method_from_package(generic, class.as_str(), package)?;
}
}

Ok(())
}
1 change: 1 addition & 0 deletions crates/ark/src/variables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
//
//

pub mod methods;
pub mod r_variables;
pub mod variable;
Loading
Loading