diff --git a/R/c-wrapper.R b/R/c-wrapper.R index 73c4e0e..bbfd83a 100644 --- a/R/c-wrapper.R +++ b/R/c-wrapper.R @@ -3,8 +3,8 @@ make_c_bridge <- function(fsub, strict = TRUE, headers = TRUE) { closure <- fsub@closure scope <- fsub@scope - uses_rng <- isTRUE(attr(scope, "uses_rng", TRUE)) - uses_errors <- isTRUE(attr(scope, "uses_errors", TRUE)) + uses_rng <- scope_uses_rng(scope) + uses_errors <- scope_uses_errors_flag(scope) fsub_arg_names <- fsub@signature # arg names closure_arg_names <- names(formals(closure)) %||% character() diff --git a/R/classes.R b/R/classes.R index bf22a76..0d2c2d2 100644 --- a/R/classes.R +++ b/R/classes.R @@ -362,6 +362,12 @@ Fortran := new_class( properties = list( value = NULL | Variable, + # Metadata flags used during compilation/lowering. Keep these as explicit + # properties rather than ad-hoc attributes so they are discoverable and + # consistently propagated with the Fortran object. + logical_booleanized = prop_bool(default = FALSE), + writes_to_dest = prop_bool(default = FALSE), + r = new_property( # custom setter only to workaround https://github.com/RConsortium/S7/issues/511 NULL | class_language | class_atomic, @@ -411,6 +417,21 @@ FortranSubroutine := new_class( ) ) +R2FHandler := new_class( + class_function, + properties = list( + dest_supported = prop_bool(default = FALSE), + dest_infer = new_property(NULL | class_function), + dest_infer_name = prop_string(default = NULL, allow_null = TRUE), + # When NULL, r2f will resolve the callable by name and use match.call(). + # When FALSE, r2f will not attempt match.call(). + match_fun = new_property( + NULL | class_function | class_logical, + default = NULL + ) + ) +) + try_prop <- function(object, name) S7::prop(object, name) %error% NULL emit <- function(..., sep = "", end = "\n") cat(..., end, sep = sep) diff --git a/R/error-handling.R b/R/error-handling.R index e42a220..3c1cf47 100644 --- a/R/error-handling.R +++ b/R/error-handling.R @@ -17,7 +17,7 @@ scope_root_for_errors <- function(scope) { return(scope) } while ( - !identical(attr(scope, "kind", exact = TRUE), "subroutine") && + !identical(scope_kind(scope), "subroutine") && inherits(parent.env(scope), "quickr_scope") ) { scope <- parent.env(scope) @@ -28,14 +28,17 @@ scope_root_for_errors <- function(scope) { mark_scope_uses_errors <- function(scope) { root <- scope_root_for_errors(scope) if (inherits(root, "quickr_scope")) { - attr(root, "uses_errors") <- TRUE + scope_mark_uses_errors_flag(root) } invisible(TRUE) } scope_uses_errors <- function(scope) { root <- scope_root_for_errors(scope) - isTRUE(attr(root, "uses_errors", TRUE)) + if (!inherits(root, "quickr_scope")) { + return(FALSE) + } + scope_uses_errors_flag(root) } fortran_string_literal <- function(x) { diff --git a/R/manifest.R b/R/manifest.R index f9c5403..7327369 100644 --- a/R/manifest.R +++ b/R/manifest.R @@ -133,7 +133,9 @@ block_tmp_allocatable <- function( max_stack_elements = block_tmp_allocatable_threshold ) { stopifnot(inherits(var, Variable)) - if (!inherits(scope, "quickr_scope") || !identical(scope@kind, "block")) { + if ( + !inherits(scope, "quickr_scope") || !identical(scope_kind(scope), "block") + ) { return(FALSE) } if (passes_as_scalar(var) || is.null(var@dims)) { @@ -347,7 +349,7 @@ r2f.scope <- function(scope, include_errors = FALSE) { local_allocs <- character() vars <- lapply(vars, function(var) { r_name <- var@r_name %||% var@name - intent_in <- r_name %in% names(formals(scope@closure)) + intent_in <- r_name %in% names(formals(scope_closure(scope))) intent_out <- (r_name %in% return_var_names) || (intent_in && var@modified) @@ -428,7 +430,7 @@ r2f.scope <- function(scope, include_errors = FALSE) { # vars that will be visible in the C bridge, either as an input or output non_local_var_names <- unique(c( - names(formals(scope@closure)), + names(formals(scope_closure(scope))), return_var_names )) @@ -437,9 +439,9 @@ r2f.scope <- function(scope, include_errors = FALSE) { var <- scope[[name]] lapply(var@dims, all.names, functions = FALSE, unique = TRUE) }))) |> - setdiff(names(formals(scope@closure))) - if (length(names(formals(scope@closure)))) { - formal_vars <- mget(names(formals(scope@closure)), scope) + setdiff(names(formals(scope_closure(scope)))) + if (length(names(formals(scope_closure(scope))))) { + formal_vars <- mget(names(formals(scope_closure(scope))), scope) formal_fortran_names <- unique(map_chr(formal_vars, \(var) { var@name %||% "" })) diff --git a/R/parallel.R b/R/parallel.R index 2919a3b..4c626fd 100644 --- a/R/parallel.R +++ b/R/parallel.R @@ -4,14 +4,14 @@ get_pending_parallel <- function(scope) { if (is.null(scope) || !inherits(scope, "quickr_scope")) { return(NULL) } - scope@pending_parallel + scope_get(scope, "pending_parallel") } has_pending_parallel <- function(scope) !is.null(get_pending_parallel(scope)) set_pending_parallel <- function(scope, decl) { stopifnot(inherits(scope, "quickr_scope"), is.list(decl)) - scope@pending_parallel <- decl + scope_set(scope, "pending_parallel", decl) invisible(scope) } @@ -19,15 +19,15 @@ take_pending_parallel <- function(scope) { if (is.null(scope) || !inherits(scope, "quickr_scope")) { return(NULL) } - decl <- scope@pending_parallel - scope@pending_parallel <- NULL + decl <- scope_get(scope, "pending_parallel") + scope_set(scope, "pending_parallel", NULL) decl } mark_openmp_used <- function(scope) { stopifnot(inherits(scope, "quickr_scope")) root <- scope_root(scope) - attr(root, "uses_openmp") <- TRUE + scope_mark_uses_openmp_flag(root) invisible(root) } @@ -35,7 +35,7 @@ scope_openmp_depth <- function(scope) { if (!inherits(scope, "quickr_scope")) { return(0L) } - depth <- attr(scope, "openmp_depth", exact = TRUE) + depth <- scope_get(scope, "openmp_depth") if (is.null(depth)) { 0L } else { @@ -51,9 +51,9 @@ enter_openmp_scope <- function(scope) { if (!inherits(scope, "quickr_scope")) { return(NULL) } - previous_depth <- attr(scope, "openmp_depth", exact = TRUE) + previous_depth <- scope_get(scope, "openmp_depth") depth <- scope_openmp_depth(scope) - attr(scope, "openmp_depth") <- depth + 1L + scope_set(scope, "openmp_depth", depth + 1L) previous_depth } @@ -62,9 +62,9 @@ exit_openmp_scope <- function(scope, previous_depth) { return(invisible(NULL)) } if (is.null(previous_depth)) { - attr(scope, "openmp_depth") <- NULL + scope_set(scope, "openmp_depth", NULL) } else { - attr(scope, "openmp_depth") <- as.integer(previous_depth) + scope_set(scope, "openmp_depth", as.integer(previous_depth)) } invisible(TRUE) } diff --git a/R/quick.R b/R/quick.R index d216ddc..3547a4b 100644 --- a/R/quick.R +++ b/R/quick.R @@ -222,7 +222,7 @@ compile <- function(fsub, build_dir = tempfile(paste0(fsub@name, "-build-"))) { FLIBS <- FLIBS[nzchar(FLIBS)] link_flags <- c(LAPACK_LIBS, BLAS_LIBS, FLIBS) - use_openmp <- isTRUE(attr(fsub@scope, "uses_openmp", exact = TRUE)) + use_openmp <- scope_uses_openmp_flag(fsub@scope) suppressWarnings({ env <- quickr_fcompiler_env( build_dir = build_dir, diff --git a/R/r2f-aaa-registry.R b/R/r2f-aaa-registry.R index c03c963..04a71f0 100644 --- a/R/r2f-aaa-registry.R +++ b/R/r2f-aaa-registry.R @@ -10,25 +10,40 @@ register_r2f_handler <- function( dest_infer = NULL, match_fun = TRUE ) { + stopifnot(is.function(fun)) + + handler <- if (inherits(fun, R2FHandler)) fun else R2FHandler(fun) + if (!is.null(dest_supported)) { - attr(fun, "dest_supported") <- dest_supported + handler@dest_supported <- isTRUE(dest_supported) } + if (!is.null(dest_infer)) { - attr(fun, "dest_infer") <- dest_infer + handler@dest_infer <- dest_infer # covr rewrites function bindings in the namespace; resolving by name at call # time ensures instrumented/rebound functions are respected. We keep the # function object for robustness (e.g., anonymous functions) and additionally # store the name when `dest_infer` is passed as a symbol. dest_infer_expr <- substitute(dest_infer) if (is.symbol(dest_infer_expr)) { - attr(fun, "dest_infer_name") <- as.character(dest_infer_expr) + handler@dest_infer_name <- as.character(dest_infer_expr) + } else { + handler@dest_infer_name <- NULL } } - if (!is.null(match_fun) && !isTRUE(match_fun)) { - attr(fun, "match.fun") <- match_fun + + if (isTRUE(match_fun)) { + handler@match_fun <- NULL + } else if ( + is.null(match_fun) || isFALSE(match_fun) || is.function(match_fun) + ) { + handler@match_fun <- match_fun + } else { + stop("match_fun must be TRUE, FALSE, NULL, or a function") } + for (nm in name) { - r2f_handlers[[nm]] <- fun + r2f_handlers[[nm]] <- handler } - invisible(fun) + invisible(handler) } diff --git a/R/r2f-aab-core.R b/R/r2f-aab-core.R index b818007..c981621 100644 --- a/R/r2f-aab-core.R +++ b/R/r2f-aab-core.R @@ -22,7 +22,7 @@ new_hoist <- function(scope) { ensure_block_scope <- function() { if (is.null(block_scope)) { - block_scope <<- scope@new_child("block") + block_scope <<- scope_new_child(scope, "block") } block_scope } @@ -81,7 +81,7 @@ logical_as_int_symbol <- function(var) { } scope_is_closure <- function(scope) { - inherits(scope, "quickr_scope") && identical(scope@kind, "closure") + inherits(scope, "quickr_scope") && identical(scope_kind(scope), "closure") } scope_fortran_names <- function(scope) { @@ -154,7 +154,11 @@ lang2fortran <- r2f <- function( { handler <- get_r2f_handler(callable_unwrapped) - match.fun <- attr(handler, "match.fun", TRUE) + match.fun <- if (inherits(handler, R2FHandler)) { + handler@match_fun + } else { + attr(handler, "match.fun", TRUE) + } if (is.null(match.fun)) { match.fun <- get0( callable_unwrapped, @@ -232,7 +236,7 @@ lang2fortran <- r2f <- function( val <- NULL } if (is.null(val) && inherits(scope, "quickr_scope")) { - closure <- scope@closure + closure <- scope_closure(scope) arg_names <- if (is.null(closure)) NULL else names(formals(closure)) if (!is.null(arg_names) && r_name %in% arg_names) { stop( @@ -255,7 +259,7 @@ lang2fortran <- r2f <- function( # and must be "booleanized" for Fortran logical operations. s <- paste0("(", s, "/=0)") out <- Fortran(s, value = if (inherits(val, Variable)) val else NULL) - attr(out, "logical_booleanized") <- TRUE + out@logical_booleanized <- TRUE out } else { Fortran(s, value = if (inherits(val, Variable)) val else NULL) @@ -375,7 +379,11 @@ dest_supported_for_call <- function(call) { return(FALSE) } handler <- get0(as.character(unwrapped[[1L]]), r2f_handlers, inherits = FALSE) - isTRUE(attr(handler, "dest_supported", exact = TRUE)) + if (inherits(handler, R2FHandler)) { + isTRUE(handler@dest_supported) + } else { + isTRUE(attr(handler, "dest_supported", exact = TRUE)) + } } dest_infer_for_call <- function(call, scope) { @@ -390,8 +398,16 @@ dest_infer_for_call <- function(call, scope) { return(NULL) } handler <- get0(as.character(unwrapped[[1L]]), r2f_handlers, inherits = FALSE) - infer <- attr(handler, "dest_infer", exact = TRUE) - infer_name <- attr(handler, "dest_infer_name", exact = TRUE) + infer <- if (inherits(handler, R2FHandler)) { + handler@dest_infer + } else { + attr(handler, "dest_infer", exact = TRUE) + } + infer_name <- if (inherits(handler, R2FHandler)) { + handler@dest_infer_name + } else { + attr(handler, "dest_infer_name", exact = TRUE) + } infer_fun <- NULL if (is_string(infer_name)) { diff --git a/R/r2f-assign.R b/R/r2f-assign.R index 0ca1775..38ac06d 100644 --- a/R/r2f-assign.R +++ b/R/r2f-assign.R @@ -102,7 +102,7 @@ register_r2f_handler( if (is_function_call(rhs)) { scope[[name]] <- as_local_closure( rhs, - environment(scope@closure), + environment(scope_closure(scope)), name = name ) return(Fortran("")) @@ -184,7 +184,7 @@ register_r2f_handler( inherits(value@value, Variable) && identical(value@value@mode, "logical") && logical_as_int(value@value) && - !isTRUE(attr(value, "logical_booleanized", exact = TRUE)) + !isTRUE(value@logical_booleanized) ) { # Keep bind(c) logicals as integer storage when the RHS is an # integer-backed expression (e.g. rev(x) for external logicals). @@ -223,7 +223,7 @@ register_r2f_handler( } # If child consumed destination (e.g., BLAS wrote directly into LHS), skip assignment - if (isTRUE(attr(value, "writes_to_dest", TRUE))) { + if (inherits(value, Fortran) && isTRUE(value@writes_to_dest)) { Fortran("") } else { Fortran(glue("{var@name} = {value}")) @@ -258,7 +258,7 @@ register_r2f_handler( register_r2f_handler( "<<-", function(args, scope, ..., hoist = NULL) { - if (is.null(scope) || !identical(scope@kind, "closure")) { + if (is.null(scope) || !identical(scope_kind(scope), "closure")) { stop("<<- is only supported inside local closures") } @@ -281,18 +281,17 @@ register_r2f_handler( stopifnot(is.symbol(target)) name <- as.character(target) - formal_names <- names(formals(scope@closure)) %||% character() + formal_names <- names(formals(scope_closure(scope))) %||% character() if (name %in% formal_names) { stop("<<- targets must not shadow closure formals: ", name) } - forbidden <- attr(scope, "forbid_superassign", exact = TRUE) %||% - character() + forbidden <- scope_forbid_superassign(scope) if (name %in% forbidden) { stop("closure must not superassign to its output variable: ", name) } - host_scope <- scope@host_scope %||% + host_scope <- scope_host_scope(scope) %||% stop("internal error: missing host scope") host_var <- get0(name, host_scope) if (!inherits(host_var, Variable)) { @@ -315,7 +314,7 @@ register_r2f_handler( register_r2f_handler( "[<<-", function(args, scope, ..., hoist = NULL) { - if (is.null(scope) || !identical(scope@kind, "closure")) { + if (is.null(scope) || !identical(scope_kind(scope), "closure")) { stop("<<- is only supported inside local closures") } @@ -328,18 +327,17 @@ register_r2f_handler( } name <- as.character(base) - formal_names <- names(formals(scope@closure)) %||% character() + formal_names <- names(formals(scope_closure(scope))) %||% character() if (name %in% formal_names) { stop("<<- targets must not shadow closure formals: ", name) } - forbidden <- attr(scope, "forbid_superassign", exact = TRUE) %||% - character() + forbidden <- scope_forbid_superassign(scope) if (name %in% forbidden) { stop("closure must not superassign to its output variable: ", name) } - host_scope <- scope@host_scope %||% + host_scope <- scope_host_scope(scope) %||% stop("internal error: missing host scope") host_var <- get0(name, host_scope) if (!inherits(host_var, Variable)) { diff --git a/R/r2f-closures.R b/R/r2f-closures.R index 54e84ce..6e01662 100644 --- a/R/r2f-closures.R +++ b/R/r2f-closures.R @@ -42,7 +42,7 @@ maybe_lower_local_closure_call <- function( } if (is_function_call(callable_unwrapped)) { - proc_name <- scope_root(scope)@get_unique_proc(prefix = "closure") + proc_name <- scope_unique_proc(scope_root(scope), prefix = "closure") host_closure <- scope_root(scope)@closure closure_obj <- as_local_closure( callable_unwrapped, @@ -85,7 +85,7 @@ as_local_closure <- function(fun_expr, env, name = NULL) { scope_root <- function(scope) { stopifnot(inherits(scope, "quickr_scope")) while ( - !identical(scope@kind, "subroutine") && + !identical(scope_kind(scope), "subroutine") && inherits(parent.env(scope), "quickr_scope") ) { scope <- parent.env(scope) @@ -129,12 +129,12 @@ compile_internal_subroutine <- function( used_names <- unique(all.vars(body(fun), functions = FALSE)) formal_scope <- new_scope(closure = NULL, parent = parent_scope) - attr(formal_scope, "kind") <- "closure_formals" + scope_set(formal_scope, "kind", "closure_formals") proc_scope <- new_scope(fun, parent = formal_scope) - attr(proc_scope, "kind") <- "closure" - attr(proc_scope, "host_scope") <- scope_root(parent_scope) - attr(proc_scope, "forbid_superassign") <- forbid_superassign + scope_set(proc_scope, "kind", "closure") + scope_set(proc_scope, "host_scope", scope_root(parent_scope)) + scope_set(proc_scope, "forbid_superassign", forbid_superassign) arg_names <- character() optional_locals <- list() @@ -845,7 +845,7 @@ compile_local_closure_proc <- function( forbid_superassign = forbid_superassign, optional_args = optional_args ) - scope_root(scope)@add_internal_proc(proc) + scope_add_internal_proc(scope_root(scope), proc) proc } @@ -1000,7 +1000,8 @@ compile_closure_call_assignment <- function( optional_args <- call_info$optional_args args_present <- call_info$args_present - return_names <- attr(scope, "return_names", exact = TRUE) %||% character() + return_names <- scope_get(scope, "return_names", default = character()) %||% + character() res_var <- if (target_exists) { out <- Variable( mode = target_var@mode, @@ -1056,7 +1057,7 @@ compile_closure_call_assignment <- function( } scope[[target_name]] <- target_var } - scope_root(scope)@add_internal_proc(proc) + scope_add_internal_proc(scope_root(scope), proc) arg_reads_target <- any(map_lgl(args_expr, function(e) { any(all.vars(e, functions = FALSE) == target_name) @@ -1139,7 +1140,7 @@ compile_sapply_assignment <- function( } } - env <- environment(scope@closure) + env <- environment(scope_closure(scope)) if (is.symbol(fun_expr)) { fun_name <- as.character(fun_expr) closure_obj <- scope[[fun_name]] @@ -1148,7 +1149,7 @@ compile_sapply_assignment <- function( } proc_name <- fun_name } else if (is_function_call(fun_expr)) { - proc_name <- scope_root(scope)@get_unique_proc(prefix = "closure") + proc_name <- scope_unique_proc(scope_root(scope), prefix = "closure") closure_obj <- as_local_closure(fun_expr, env, name = proc_name) } else { stop("unsupported FUN in sapply(); use a local closure or function(i) ...") @@ -1191,7 +1192,8 @@ compile_sapply_assignment <- function( iterable_len_expr <- value_length_expr(iterable_value) - iterable_tmp <- scope@get_unique_var( + iterable_tmp <- scope_unique_var( + scope, mode = iterable_value@mode, dims = iterable_value@dims ) @@ -1233,7 +1235,8 @@ compile_sapply_assignment <- function( } res_var <- inferred - return_names <- attr(scope, "return_names", exact = TRUE) %||% character() + return_names <- scope_get(scope, "return_names", default = character()) %||% + character() if (res_var@mode == "logical" && out_name %in% return_names) { res_var@logical_as_int <- TRUE proc <- compile_internal_subroutine( @@ -1266,7 +1269,7 @@ compile_sapply_assignment <- function( scope[[out_name]] <- out_var } - scope_root(scope)@add_internal_proc(proc) + scope_add_internal_proc(scope_root(scope), proc) out_target <- out_name post_stmts <- character() @@ -1280,7 +1283,7 @@ compile_sapply_assignment <- function( post_stmts <- glue("{out_name} = {out_target}") } - idx <- scope@get_unique_var("integer") + idx <- scope_unique_var(scope, "integer") last_i <- if (index_iterable) { if (is.null(iterable_len_expr) || is_scalar_na(iterable_len_expr)) { NULL @@ -1452,7 +1455,7 @@ compile_subset_designator <- function( # Convert logical vectors to integer vector subscripts (R's `which()`). # Fortran array designators do not accept logical vectors directly. mask <- booleanize_logical_as_int(subscript) - it <- scope@get_unique_var("integer") + it <- scope_unique_var(scope, "integer") f <- glue("pack([({it}, {it}=1, size({mask}))], {mask})") Fortran(f, Variable("int", NA)) }, @@ -1552,7 +1555,7 @@ compile_subscript_lhs <- function( return(list(pre = pre, lhs = Fortran(designator))) } - if (is.null(scope) || !identical(scope@kind, "closure")) { + if (is.null(scope) || !identical(scope_kind(scope), "closure")) { stop("host-target subset compilation is only valid inside local closures") } @@ -1562,7 +1565,8 @@ compile_subscript_lhs <- function( } name <- as.character(base) - host_scope <- scope@host_scope %||% stop("internal error: missing host scope") + host_scope <- scope_host_scope(scope) %||% + stop("internal error: missing host scope") host_var <- get0(name, host_scope) if (!inherits(host_var, Variable)) { stop( diff --git a/R/r2f-constructors.R b/R/r2f-constructors.R index d1ec2c7..6fad92d 100644 --- a/R/r2f-constructors.R +++ b/R/r2f-constructors.R @@ -95,7 +95,7 @@ r2f_handlers[["rep.int"]] <- function(args, scope, ..., hoist = NULL) { len_expr <- NA_integer_ } - i <- scope@get_unique_var("integer") + i <- scope_unique_var(scope, "integer") out_val <- Variable("integer", list(len_expr)) Fortran(glue("[({x}, {i}=1, int({times}, kind=c_int))]"), out_val) } @@ -348,7 +348,7 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { } source <- if (is_fill_constructor) { - i <- scope@get_unique_var("integer") + i <- scope_unique_var(scope, "integer") glue("[({out}, {i}=1, int({n_expr}))]") } else { n_target <- known_prod(target_dims) diff --git a/R/r2f-control-flow.R b/R/r2f-control-flow.R index 658e02a..0a14d59 100644 --- a/R/r2f-control-flow.R +++ b/R/r2f-control-flow.R @@ -131,14 +131,15 @@ r2f_handlers[["for"]] <- function(args, scope, ...) { loop_var@modified <- TRUE scope[[var]] <- loop_var - iterable_tmp <- scope@get_unique_var( + iterable_tmp <- scope_unique_var( + scope, mode = iterable_var@mode, dims = iterable_var@dims, logical_as_int = logical_as_int(iterable_var) ) iterable_tmp_assign <- glue("{iterable_tmp@name} = {iterable_var@name}") - idx <- scope@get_unique_var("integer") + idx <- scope_unique_var(scope, "integer") end <- if (passes_as_scalar(iterable_var)) { "1_c_int" } else { diff --git a/R/r2f-core-handlers.R b/R/r2f-core-handlers.R index 2b16131..905644f 100644 --- a/R/r2f-core-handlers.R +++ b/R/r2f-core-handlers.R @@ -16,7 +16,7 @@ r2f_handlers[["declare"]] <- function(args, scope, ...) { } else if (is_type_call(a)) { var <- type_call_to_var(a) r_name <- var@r_name %||% var@name - var@is_arg <- r_name %in% names(formals(scope@closure)) + var@is_arg <- r_name %in% names(formals(scope_closure(scope))) if (identical(var@mode, "logical") && isTRUE(var@is_arg)) { var@logical_as_int <- TRUE } diff --git a/R/r2f-iterables-helpers.R b/R/r2f-iterables-helpers.R index e31ecc5..11fe3f6 100644 --- a/R/r2f-iterables-helpers.R +++ b/R/r2f-iterables-helpers.R @@ -286,7 +286,7 @@ seq_like_r2f <- function( glue("{start}:{end}:{step}") } } else { - i <- scope@get_unique_var("integer") + i <- scope_unique_var(scope, "integer") fr <- if (omit_step) { glue("[ ({i}, {i} = {start}, {end}) ]") } else { diff --git a/R/r2f-matrix-blas.R b/R/r2f-matrix-blas.R index 5b8a4fe..e1b694b 100644 --- a/R/r2f-matrix-blas.R +++ b/R/r2f-matrix-blas.R @@ -346,7 +346,7 @@ gemm <- function( "call dgemm('{opA}','{opB}', {blas_int(m)}, {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {B_name}, {blas_int(ldb)}, 0.0_c_double, {dest@name}, {blas_int(ldc_expr)})" )) out <- Fortran(dest@name, dest) - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE return(out) } @@ -391,7 +391,7 @@ gemv <- function( "call dgemv('{transA}', {blas_int(m)}, {blas_int(n)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {x_name}, 1_c_int, 0.0_c_double, {dest@name}, 1_c_int)" )) out <- Fortran(dest@name, dest) - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE return(out) } # Else assign to a temporary variable @@ -505,7 +505,7 @@ syrk <- function( out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -547,7 +547,7 @@ outer_mul <- function( "call dger({blas_int(m)}, {blas_int(n)}, 1.0_c_double, {x_name}, 1_c_int, {y_name}, 1_c_int, {dest@name}, {blas_int(m)})" )) out <- Fortran(dest@name, dest) - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE return(out) } @@ -651,7 +651,7 @@ triangular_solve <- function( out <- Fortran(B_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -757,7 +757,7 @@ lapack_solve <- function( out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } return(out) } @@ -872,7 +872,7 @@ end do" out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } return(out) } @@ -988,7 +988,7 @@ end do" out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -1062,7 +1062,7 @@ lapack_inverse <- function(A, scope, hoist, dest = NULL, context = "solve") { out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -1119,7 +1119,7 @@ lapack_chol <- function(A, scope, hoist, dest = NULL, context = "chol") { out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -1182,7 +1182,7 @@ lapack_chol2inv <- function( out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -1225,7 +1225,7 @@ end do" out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } @@ -1288,7 +1288,7 @@ end do" out <- Fortran(out_name, out_var) if (writes_to_dest) { - attr(out, "writes_to_dest") <- TRUE + out@writes_to_dest <- TRUE } out } diff --git a/R/r2f-matrix.R b/R/r2f-matrix.R index 9d669f1..4536ee6 100644 --- a/R/r2f-matrix.R +++ b/R/r2f-matrix.R @@ -1001,9 +1001,9 @@ svd_output_vars_scope <- function(scope, dims) { all(c("m", "n", "mn") %in% names(dims)) ) list( - d = scope@get_unique_var(mode = "double", dims = list(dims$mn)), - u = scope@get_unique_var(mode = "double", dims = list(dims$m, dims$mn)), - v = scope@get_unique_var(mode = "double", dims = list(dims$n, dims$mn)) + d = scope_unique_var(scope, mode = "double", dims = list(dims$mn)), + u = scope_unique_var(scope, mode = "double", dims = list(dims$m, dims$mn)), + v = scope_unique_var(scope, mode = "double", dims = list(dims$n, dims$mn)) ) } diff --git a/R/r2f-operators-helpers.R b/R/r2f-operators-helpers.R index d5a5d6b..b1ce6bd 100644 --- a/R/r2f-operators-helpers.R +++ b/R/r2f-operators-helpers.R @@ -16,12 +16,12 @@ booleanize_logical_as_int <- function(x) { return(x) } - if (isTRUE(attr(x, "logical_booleanized", exact = TRUE))) { + if (isTRUE(x@logical_booleanized)) { return(x) } out <- Fortran(glue("({x} /= 0)"), Variable("logical", x@value@dims)) - attr(out, "logical_booleanized") <- TRUE + out@logical_booleanized <- TRUE out } diff --git a/R/r2f-random.R b/R/r2f-random.R index 8d2cb0e..309f11d 100644 --- a/R/r2f-random.R +++ b/R/r2f-random.R @@ -4,7 +4,7 @@ # --- Handlers --- r2f_handlers[["runif"]] <- function(args, scope, ..., hoist = NULL) { - attr(scope, "uses_rng") <- TRUE + scope_mark_uses_rng(scope) dims <- r2dims(args$n, scope) var <- Variable("double", dims) @@ -28,7 +28,7 @@ r2f_handlers[["runif"]] <- function(args, scope, ..., hoist = NULL) { if (passes_as_scalar(var)) { fortran <- get1rand } else { - tmp_i <- scope@get_unique_var("integer") ## would be better as uint64... + tmp_i <- scope_unique_var(scope, "integer") ## would be better as uint64... fortran <- glue("[({get1rand}, {tmp_i}=1, {dims[[1L]]})]") } diff --git a/R/r2f-reductions.R b/R/r2f-reductions.R index d5d5d58..017bba3 100644 --- a/R/r2f-reductions.R +++ b/R/r2f-reductions.R @@ -256,7 +256,7 @@ r2f_handlers[["which.max"]] <- r2f_handlers[["which.min"]] <- use_lgl_storage <- has_var_name && !logical_as_int(x@value) int_backed_expr <- logical_as_int(x@value) && - !isTRUE(attr(x, "logical_booleanized", exact = TRUE)) + !isTRUE(x@logical_booleanized) # Prefer searching the underlying integer storage directly when available # (external logical arrays are passed as integer(0/1)). If the input is an diff --git a/R/r2f-subscript.R b/R/r2f-subscript.R index fb36159..dbd443e 100644 --- a/R/r2f-subscript.R +++ b/R/r2f-subscript.R @@ -121,7 +121,7 @@ r2f_handlers[["["]] <- function( }, logical1 = { # we convert to a temp integer vector, doing the equivalent of R's which() - i <- scope@get_unique_var("integer") + i <- scope_unique_var(scope, "integer") f <- glue("pack([({i}, {i}=1, size({subscript}))], {subscript})") return(Fortran(f, Variable("int", NA))) }, diff --git a/R/scope-helpers.R b/R/scope-helpers.R new file mode 100644 index 0000000..7493276 --- /dev/null +++ b/R/scope-helpers.R @@ -0,0 +1,98 @@ +# Scope helpers. +# +# quickr scopes are environments with reference semantics. Historically, we +# stored a lot of mutable state in ad-hoc env attributes and accessed it via a +# custom `@.quickr_scope` method, which can be confused with S7 `@` property +# access. These helpers make scope state access explicit and grep-able. + +scope_state <- function(scope) { + stopifnot(inherits(scope, "quickr_scope")) + st <- attr(scope, "state", exact = TRUE) + stopifnot(is.environment(st)) + st +} + +scope_get <- function(scope, name, default = NULL) { + stopifnot(inherits(scope, "quickr_scope"), is_string(name)) + st <- scope_state(scope) + if (!exists(name, envir = st, inherits = FALSE)) { + return(default) + } + get(name, envir = st, inherits = FALSE) +} + +scope_set <- function(scope, name, value) { + stopifnot(inherits(scope, "quickr_scope"), is_string(name)) + st <- scope_state(scope) + assign(name, value, envir = st) + invisible(scope) +} + +scope_kind <- function(scope) { + scope_get(scope, "kind") +} + +scope_closure <- function(scope) { + scope_get(scope, "closure") +} + +scope_uses_rng <- function(scope) { + isTRUE(scope_get(scope, "uses_rng", FALSE)) +} + +scope_mark_uses_rng <- function(scope) { + scope_set(scope, "uses_rng", TRUE) + invisible(TRUE) +} + +scope_uses_errors_flag <- function(scope) { + isTRUE(scope_get(scope, "uses_errors", FALSE)) +} + +scope_mark_uses_errors_flag <- function(scope) { + scope_set(scope, "uses_errors", TRUE) + invisible(TRUE) +} + +scope_uses_openmp_flag <- function(scope) { + isTRUE(scope_get(scope, "uses_openmp", FALSE)) +} + +scope_mark_uses_openmp_flag <- function(scope) { + scope_set(scope, "uses_openmp", TRUE) + invisible(TRUE) +} + +scope_forbid_superassign <- function(scope) { + scope_get(scope, "forbid_superassign", character()) +} + +scope_host_scope <- function(scope) { + scope_get(scope, "host_scope") +} + +scope_new_child <- function(scope, kind) { + stopifnot(inherits(scope, "quickr_scope"), is_string(kind)) + f <- scope_get(scope, "new_child") + stopifnot(is.function(f)) + f(kind) +} + +scope_unique_var <- function(scope, ...) { + f <- scope_get(scope, "get_unique_var") + stopifnot(is.function(f)) + f(...) +} + +scope_unique_proc <- function(scope, prefix = "closure") { + stopifnot(is_string(prefix)) + f <- scope_get(scope, "get_unique_proc") + stopifnot(is.function(f)) + f(prefix = prefix) +} + +scope_add_internal_proc <- function(scope, proc) { + f <- scope_get(scope, "add_internal_proc") + stopifnot(is.function(f)) + f(proc) +} diff --git a/R/scope.R b/R/scope.R index a94d812..192da58 100644 --- a/R/scope.R +++ b/R/scope.R @@ -10,7 +10,11 @@ new_ordered_env <- function(parent = emptyenv()) { # Allow scopes to pre-declare which symbols are "return" variables (external # outputs). When logical, quickr represents these using integer storage. if (inherits(value, Variable)) { - return_names <- attr(x, "return_names", exact = TRUE) + return_names <- if (inherits(x, "quickr_scope")) { + scope_get(x, "return_names") + } else { + attr(x, "return_names", exact = TRUE) + } if (!is.null(return_names) && as.character(name) %in% return_names) { value@is_return <- TRUE if (identical(value@mode, "logical")) { @@ -68,16 +72,19 @@ check_assignment_compatible <- function(target, value) { new_scope <- function(closure, parent = emptyenv()) { scope <- new_ordered_env(parent = parent) class(scope) <- unique(c("quickr_scope", class(scope))) - attr(scope, "closure") <- closure - attr(scope, "kind") <- if (is.null(closure)) "block" else "subroutine" - attr(scope, "return_names") <- character() - attr(scope, "internal_procs") <- list() + state <- new.env(parent = emptyenv()) + attr(scope, "state") <- state + + state$closure <- closure + state$kind <- if (is.null(closure)) "block" else "subroutine" + state$return_names <- character() + state$internal_procs <- list() - attr(scope, "get_unique_var") <- local({ + state$get_unique_var <- local({ i <- 0L function(...) { prefix <- switch( - attr(scope, "kind", exact = TRUE) %||% "subroutine", + state$kind %||% "subroutine", block = "btmp", closure = "ctmp", subroutine = "tmp", @@ -88,7 +95,7 @@ new_scope <- function(closure, parent = emptyenv()) { } }) - attr(scope, "get_unique_proc") <- local({ + state$get_unique_proc <- local({ i <- 0L function(prefix = "closure") { stopifnot(is_string(prefix)) @@ -96,14 +103,14 @@ new_scope <- function(closure, parent = emptyenv()) { } }) - attr(scope, "new_child") <- function(kind = c("block", "closure")) { + state$new_child <- function(kind = c("block", "closure")) { kind <- match.arg(kind) child <- new_scope(closure = NULL, parent = scope) - attr(child, "kind") <- kind + scope_set(child, "kind", kind) child } - attr(scope, "assign") <- function(name, value) { + state$assign <- function(name, value) { stopifnot(inherits(value, Variable), is.symbol(name) || is_string(name)) name <- as.character(name) existing <- get0(name, scope) @@ -114,11 +121,11 @@ new_scope <- function(closure, parent = emptyenv()) { assign(name, value, scope) } - attr(scope, "add_internal_proc") <- function(proc) { + state$add_internal_proc <- function(proc) { stopifnot(is.list(proc), is_string(proc$name), is_string(proc$code)) - procs <- attr(scope, "internal_procs", exact = TRUE) %||% list() + procs <- state$internal_procs %||% list() procs[[proc$name]] <- proc - attr(scope, "internal_procs") <- procs + state$internal_procs <- procs invisible(proc) } @@ -127,12 +134,12 @@ new_scope <- function(closure, parent = emptyenv()) { scope_return_var_names <- function(scope) { stopifnot(inherits(scope, "quickr_scope")) - return_var_names <- closure_return_var_names(scope@closure) + return_var_names <- closure_return_var_names(scope_closure(scope)) if (!length(return_var_names)) { return(return_var_names) } - is_list_return <- is_call(last(body(scope@closure)), quote(list)) + is_list_return <- is_call(last(body(scope_closure(scope))), quote(list)) values <- unname(return_var_names) names_in <- names(return_var_names) if (is.null(names_in)) { @@ -172,13 +179,25 @@ scope_return_var_names <- function(scope) { #' @export -`@.quickr_scope` <- function(x, name) attr(x, name, exact = TRUE) +`@.quickr_scope` <- function(x, name) { + name <- as.character(name) + scope_get(x, name) +} #' @export -`@<-.quickr_scope` <- function(x, name, value) `attr<-`(x, name, value = value) +`@<-.quickr_scope` <- function(x, name, value) { + name <- as.character(name) + scope_set(x, name, value) + x +} #' @importFrom utils .AtNames findMatches #' @export .AtNames.quickr_scope <- function(x, pattern = "") { - findMatches(pattern, names(attributes(x))) + nms <- names(attributes(x)) %||% character() + st <- attr(x, "state", exact = TRUE) + if (is.environment(st)) { + nms <- unique(c(nms, ls(envir = st, all.names = TRUE))) + } + findMatches(pattern, nms) } diff --git a/R/subroutine.R b/R/subroutine.R index 586ac20..3382775 100644 --- a/R/subroutine.R +++ b/R/subroutine.R @@ -20,9 +20,11 @@ new_fortran_subroutine <- function( # body <- rlang::zap_srcref(body) scope <- new_scope(closure, parent) - attr(scope, "return_names") <- unique(unname(closure_return_var_names( - closure - ))) + scope_set( + scope, + "return_names", + unique(unname(closure_return_var_names(closure))) + ) # inject symbols for var sizes in declare calls, so like: # declare(type(foo = integer(nr, NA)), @@ -58,14 +60,15 @@ new_fortran_subroutine <- function( } } - uses_errors <- isTRUE(attr(scope, "uses_errors", TRUE)) - uses_openmp <- isTRUE(attr(scope, "uses_openmp", TRUE)) + uses_errors <- scope_uses_errors_flag(scope) + uses_openmp <- scope_uses_openmp_flag(scope) manifest <- r2f.scope(scope, include_errors = uses_errors) local_allocs <- attr(manifest, "local_allocations", exact = TRUE) %||% character() fsub_arg_names <- attr(manifest, "signature", TRUE) - internal_procs <- attr(scope, "internal_procs", exact = TRUE) %||% list() + internal_procs <- scope_get(scope, "internal_procs", default = list()) %||% + list() contains_entries <- c( lapply(internal_procs, `[[`, "code") |> unlist(use.names = FALSE), @@ -92,7 +95,7 @@ new_fortran_subroutine <- function( body_section <- str_flatten_lines(body_section, "", contains_block_indented) } - uses_rng <- isTRUE(attr(scope, 'uses_rng', TRUE)) + uses_rng <- scope_uses_rng(scope) used_iso_bindings <- iso_c_binding_symbols( vars = scope_vars(scope), body_code = body, diff --git a/tests/testthat/test-r2f-registry.R b/tests/testthat/test-r2f-registry.R index f664134..67aeaa8 100644 --- a/tests/testthat/test-r2f-registry.R +++ b/tests/testthat/test-r2f-registry.R @@ -7,10 +7,11 @@ test_that("register_r2f_handler sets dest_supported attribute", { handler, dest_supported = TRUE ) - expect_identical(attr(result, "dest_supported"), TRUE) + expect_true(inherits(result, quickr:::R2FHandler)) + expect_identical(result@dest_supported, TRUE) }) -test_that("register_r2f_handler sets dest_infer attribute", { +test_that("register_r2f_handler stores dest_infer metadata", { handler <- function(e, scope, ...) NULL infer_fn <- function(args, scope) NULL result <- quickr:::register_r2f_handler( @@ -18,8 +19,9 @@ test_that("register_r2f_handler sets dest_infer attribute", { handler, dest_infer = infer_fn ) - expect_identical(attr(result, "dest_infer"), infer_fn) - expect_identical(attr(result, "dest_infer_name"), "infer_fn") + expect_true(inherits(result, quickr:::R2FHandler)) + expect_identical(result@dest_infer, infer_fn) + expect_identical(result@dest_infer_name, "infer_fn") }) test_that("register_r2f_handler keeps anonymous dest_infer without a name", { @@ -30,8 +32,9 @@ test_that("register_r2f_handler keeps anonymous dest_infer without a name", { handler, dest_infer = (function(args, scope) infer_fn(args, scope)) ) - expect_true(is.function(attr(result, "dest_infer"))) - expect_null(attr(result, "dest_infer_name")) + expect_true(inherits(result, quickr:::R2FHandler)) + expect_true(is.function(result@dest_infer)) + expect_null(result@dest_infer_name) }) test_that("register_r2f_handler sets match.fun attribute when not TRUE", { @@ -42,7 +45,8 @@ test_that("register_r2f_handler sets match.fun attribute when not TRUE", { handler, match_fun = match_fn ) - expect_identical(attr(result, "match.fun"), match_fn) + expect_true(inherits(result, quickr:::R2FHandler)) + expect_identical(result@match_fun, match_fn) }) test_that("register_r2f_handler does not set match.fun when TRUE", { @@ -52,7 +56,8 @@ test_that("register_r2f_handler does not set match.fun when TRUE", { handler, match_fun = TRUE ) - expect_null(attr(result, "match.fun")) + expect_true(inherits(result, quickr:::R2FHandler)) + expect_null(result@match_fun) }) test_that("register_r2f_handler registers multiple names", { diff --git a/tests/testthat/test-scope-state.R b/tests/testthat/test-scope-state.R new file mode 100644 index 0000000..8d13dd6 --- /dev/null +++ b/tests/testthat/test-scope-state.R @@ -0,0 +1,19 @@ +# Exercise quickr_scope state accessors to keep refactors covered by covr. + +test_that("quickr_scope @ access reads and writes state", { + scope <- quickr:::new_scope(function(x) x, parent = emptyenv()) + + expect_identical(scope@kind, "subroutine") + expect_true(is.function(scope@closure)) + + expect_false(quickr:::scope_uses_rng(scope)) + scope@uses_rng <- TRUE + expect_true(quickr:::scope_uses_rng(scope)) +}) + +test_that(".AtNames.quickr_scope includes state fields", { + scope <- quickr:::new_scope(NULL, parent = emptyenv()) + nms <- quickr:::.AtNames.quickr_scope(scope) + expect_true("kind" %in% nms) + expect_true("get_unique_var" %in% nms) +})