Skip to content

Commit 599bd98

Browse files
authored
Merge pull request #49 from mns-nordicals/list_return_1
Add ability to return multiple values
2 parents 6ac1924 + 544bf69 commit 599bd98

8 files changed

Lines changed: 368 additions & 33 deletions

File tree

NEWS.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# quickr (development version)
22

3+
- Functions can now return multiple arrays in a `list()`, optionally
4+
named (#49, @mns-nordicals).
5+
36
# quickr 0.2.0
47

58
- Internal utility `r2f()` print method now shows the generated `c_bridge`
@@ -28,12 +31,12 @@
2831

2932
- Added workaround for cases where the compiler error message might not
3033
display correctly in RStudio.
31-
34+
3235
- Improved error message when using case-sensitive variable names (#18, #36, #39)
3336

34-
- Added `AGENTS.md` and `scripts/setup_codex.sh` to enable the ChatGPT/Codex agent
37+
- Added `AGENTS.md` and `scripts/setup_codex.sh` to enable the ChatGPT/Codex agent
3538
to run tests in a docker container configured without internet access.
36-
39+
3740

3841
# quickr 0.1.0
3942

R/c-wrapper.R

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,19 @@ make_c_bridge <- function(fsub, strict = TRUE, headers = TRUE) {
3636
scope = scope
3737
)
3838

39-
# maybe define and allocate the output var
39+
# maybe define and allocate the output var(s)
4040
n_protected <- 0L
41-
return_var <- get(closure_return_var_name(closure), scope)
42-
if (!return_var@name %in% closure_arg_names) {
43-
return_var@modified <- TRUE
44-
assign(return_var@name, return_var, scope)
45-
append(c_body) <- return_var_c_defs(return_var, fsub@scope)
46-
add(n_protected) <- 1L # allocated return var
47-
if (return_var@rank > 1) {
48-
add(n_protected) <- 1L # allocated _dim_sexp
41+
return_var_names <- closure_return_var_names(closure)
42+
# Deduplicate by the underlying variable name to avoid duplicate C defs
43+
for (return_var in mget(unique(unname(return_var_names)), scope)) {
44+
if (!return_var@name %in% closure_arg_names) {
45+
return_var@modified <- TRUE
46+
assign(return_var@name, return_var, scope)
47+
append(c_body) <- return_var_c_defs(return_var, fsub@scope)
48+
add(n_protected) <- 1L # allocated return var
49+
if (return_var@rank > 1) {
50+
add(n_protected) <- 1L # allocated _dim_sexp
51+
}
4952
}
5053
}
5154

@@ -64,10 +67,49 @@ make_c_bridge <- function(fsub, strict = TRUE, headers = TRUE) {
6467
if (uses_rng) "PutRNGstate();",
6568
""
6669
)
67-
if (n_protected > 0) {
68-
append(c_body) <- glue("UNPROTECT({n_protected});")
70+
# Determine if the closure returns a list call or a single symbol
71+
is_list_return <- is_call(last(body(closure)), quote(list))
72+
73+
if (length(return_var_names) == 1L && !is_list_return) {
74+
if (n_protected > 0) {
75+
append(c_body) <- glue("UNPROTECT({n_protected});")
76+
}
77+
append(c_body) <- glue("return {return_var_names};")
78+
} else {
79+
return_var_values <- unname(return_var_names)
80+
provided_names <- names(return_var_names)
81+
if (is.null(provided_names)) {
82+
provided_names <- rep("", length(return_var_values))
83+
}
84+
has_any_names <- any(nzchar(provided_names))
85+
86+
append(c_body) <- c(
87+
glue(
88+
"SEXP _ans = PROTECT(Rf_allocVector(VECSXP, {length(return_var_values)}));"
89+
),
90+
imap(return_var_values, function(nm, i) {
91+
glue("SET_VECTOR_ELT(_ans, {i-1}, {nm});")
92+
})
93+
)
94+
95+
if (has_any_names) {
96+
names_to_use <- provided_names
97+
append(c_body) <- c(
98+
glue(
99+
"SEXP _names = PROTECT(Rf_allocVector(STRSXP, {length(return_var_values)}));"
100+
),
101+
imap(names_to_use, function(nm, i) {
102+
glue('SET_STRING_ELT(_names, {i-1}, Rf_mkChar("{nm}"));')
103+
}),
104+
"Rf_setAttrib(_ans, R_NamesSymbol, _names);"
105+
)
106+
append(c_body) <- glue("UNPROTECT({n_protected + 2});")
107+
} else {
108+
append(c_body) <- glue("UNPROTECT({n_protected + 1});")
109+
}
110+
111+
append(c_body) <- "return _ans;"
69112
}
70-
append(c_body) <- glue("return {return_var@name};")
71113

72114
c_args <- paste("SEXP", names(formals(closure)), collapse = ", ")
73115
c_body <- as_glue(str_flatten_lines(c_body))
@@ -416,12 +458,39 @@ as_friendly_size_expression <- function(d) {
416458
deparse1(d)
417459
}
418460

419-
closure_return_var_name <- function(closure) {
420-
return_var_name <- last(body(closure))
421-
if (!is.symbol(return_var_name)) {
422-
stop("return value must be a symbol")
461+
closure_return_var_names <- function(closure) {
462+
return_var_expr <- last(body(closure))
463+
if (is.symbol(return_var_expr)) {
464+
val <- as.character(return_var_expr)
465+
# Return named to keep interface consistent
466+
return(setNames(val, val))
467+
}
468+
if (is_call(return_var_expr, quote(list))) {
469+
args <- as.list(return_var_expr)[-1L]
470+
if (length(args) == 0L) {
471+
stop("return list must contain at least one element")
472+
}
473+
vals <- map_chr(args, as.character)
474+
nms <- names(args)
475+
if (is.null(nms)) {
476+
nms <- rep("", length(vals))
477+
}
478+
# validate names are syntactic when provided
479+
if (any(nzchar(nms))) {
480+
bad <- nzchar(nms) & make.names(nms) != nms
481+
if (any(bad)) {
482+
stop(
483+
"only syntactic names are valid, encountered: ",
484+
paste0(nms[bad], sep = ", ")
485+
)
486+
}
487+
}
488+
# Use provided names when present; fallback to symbol names
489+
# nms <- ifelse(nzchar(nms), nms, vals)
490+
return(setNames(vals, nms))
423491
}
424-
as.character(return_var_name)
492+
## is it redundent ? new_fortran_subroutine also errors ?
493+
stop("return value must be a symbol or list of symbols")
425494
}
426495

427496

R/manifest.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ r2f.scope <- function(scope) {
3838
vars <- as.list.environment(scope, all.names = TRUE)
3939
vars <- lapply(vars, function(var) {
4040
intent_in <- var@name %in% names(formals(scope@closure))
41-
intent_out <- var@name == closure_return_var_name(scope@closure) ||
42-
intent_in && var@modified
41+
intent_out <-
42+
(var@name %in% closure_return_var_names(scope@closure)) ||
43+
(intent_in && var@modified)
4344

4445
intent <-
4546
if (intent_in && intent_out) {
@@ -88,7 +89,7 @@ r2f.scope <- function(scope) {
8889
# vars that will be visible in the C bridge, either as an input or output
8990
non_local_var_names <- unique(c(
9091
names(formals(scope@closure)),
91-
closure_return_var_name(scope@closure)
92+
closure_return_var_names(scope@closure)
9293
))
9394

9495
# collect all size_names; sort so non-locals are declared first.

R/preprocess-lang.R

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,57 @@ defuse_numeric_literals <- function(e) {
1212
e
1313
}
1414

15+
# Helper function to validate that all list elements are symbols
16+
validate_list_symbols <- function(list_call) {
17+
args <- as.list(list_call)[-1L]
18+
if (!all(map_lgl(args, is.symbol))) {
19+
stop("all elements of the list must be symbols")
20+
}
21+
invisible(TRUE)
22+
}
23+
1524

1625
ensure_last_expr_sym <- function(bdy) {
1726
if (!is_call(bdy, quote(`{`))) {
1827
stop("bad body, needs {")
1928
}
20-
if (!is.symbol(last_expr <- last(bdy))) {
21-
bdy[[length(bdy)]] <- call("<-", quote(out_), last_expr)
22-
bdy[[length(bdy) + 1L]] <- quote(out_)
29+
30+
last_expr <- last(bdy)
31+
32+
# Case 1: Last expression is a symbol
33+
if (is.symbol(last_expr)) {
34+
# Check for pattern: out <- list(...); out
35+
n <- length(bdy)
36+
second_last_expr <- bdy[[n - 1L]]
37+
38+
list_pattern <-
39+
is_call(second_last_expr, quote(`<-`)) &&
40+
identical(second_last_expr[[2L]], last_expr) &&
41+
is_call(second_last_expr[[3L]], quote(list))
42+
43+
if (list_pattern) {
44+
# Modify body such that last espression is list(...)
45+
list_call <- second_last_expr[[3L]]
46+
validate_list_symbols(list_call)
47+
bdy[[n - 1]] <- NULL # delete list assignment
48+
bdy[[n - 1]] <- list_call # replace last line with list(...)
49+
}
50+
51+
return(bdy)
52+
}
53+
54+
# Case 2: Last expression is a direct list call
55+
if (is_call(last_expr, quote(list))) {
56+
validate_list_symbols(last_expr)
57+
return(bdy)
2358
}
59+
60+
# Case 3: Other expressions - create assignment to out_
61+
bdy[[length(bdy)]] <- call("<-", quote(out_), last_expr)
62+
bdy[[length(bdy) + 1L]] <- quote(out_)
2463
bdy
2564
}
2665

27-
2866
whole_doubles_to_ints <- function(x) {
2967
walker <- function(x) {
3068
switch(
@@ -38,7 +76,6 @@ whole_doubles_to_ints <- function(x) {
3876
walker(x)
3977
}
4078

41-
4279
substitute_unique_case_insensitive_symbols <- function(x) {
4380
# TODO: would be nice to fix case-insenstive name clashes
4481
# with automatic substitutions. Would be a little involved since

R/subroutine.R

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,24 @@ new_fortran_subroutine <- function(name, closure, parent = emptyenv()) {
3535
}
3636
}
3737

38-
# figure out the return variable.
39-
if (is.symbol(last_expr <- last(body(closure)))) {
38+
# figure out the return variable(s).
39+
last_expr <- last(body(closure))
40+
if (is.symbol(last_expr)) {
4041
return_var <- get(last_expr, scope)
4142
return_var@is_return <- TRUE
4243
scope[[as.character(last_expr)]] <- return_var
44+
} else if (is_call(last_expr, quote(list))) {
45+
args <- as.list(last_expr)[-1L]
46+
for (arg in args) {
47+
var <- get(arg, scope)
48+
var@is_return <- TRUE
49+
scope[[as.character(arg)]] <- var
50+
}
4351
} else {
4452
# lots we can still do here, just not implemented yet.
45-
stop("last expression in the function must be a bare symbol")
53+
stop(
54+
"last expression in the function must be a bare symbol or list of symbols"
55+
)
4656
}
4757

4858
manifest <- r2f.scope(scope)

tests/testthat/test-as-double.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ test_that("/ performs real division for integer and logical inputs", {
2424
a / b
2525
}
2626
expect_quick_equal(div_lgl, list(c(TRUE, FALSE, TRUE), c(TRUE, TRUE, TRUE)))
27-
})
27+
})

tests/testthat/test-loops.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,3 @@ test_that("expr return value", {
101101
expect_translation_snapshots(fn)
102102
expect_quick_identical(fn, 1:10)
103103
})
104-

0 commit comments

Comments
 (0)