Skip to content

Add ability to return multiple values - #49

Merged
t-kalinowski merged 10 commits into
t-kalinowski:mainfrom
mns-nordicals:list_return_1
Sep 2, 2025
Merged

Add ability to return multiple values#49
t-kalinowski merged 10 commits into
t-kalinowski:mainfrom
mns-nordicals:list_return_1

Conversation

@mns-nordicals

@mns-nordicals mns-nordicals commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

This pull request adds the ability to return multiple values from a quickr function.

I'll admit that it was mostly written by AI, but in my testing it seem to work well. It clears all test and I have added a new test file with a few tests.

This closes #50

@t-kalinowski
t-kalinowski requested a review from Copilot August 26, 2025 12:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request adds support for returning multiple values from quickr functions by allowing the last expression to be a list of symbols instead of just a single symbol. The implementation handles both direct list returns and assignment-based returns, extending the existing single return value functionality.

  • Support for multiple return values through list syntax
  • Enhanced preprocessing to handle assignment patterns
  • Updated C bridge generation for multi-value returns

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/testthat/test-multi-return.R Adds comprehensive tests for multiple return values functionality
R/subroutine.R Extends return variable detection to handle list expressions
R/preprocess-lang.R Adds preprocessing logic for assignment-based multi-returns
R/manifest.R Updates scope processing to use plural return variable names
R/c-wrapper.R Implements C bridge generation for multiple return values

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread R/preprocess-lang.R
Comment on lines +21 to +41
if (is.symbol(last_expr)) {
n <- length(bdy)
if (n >= 3L) {
prev_expr <- bdy[[n - 1L]]
if (
is_call(prev_expr, quote(`<-`)) &&
identical(prev_expr[[2L]], last_expr) &&
is_call(prev_expr[[3L]], quote(list))
) {
args <- as.list(prev_expr[[3L]])[-1L]
if (!all(map_lgl(args, is.symbol))) {
stop("all elements of return list must be symbols")
}
bdy_list <- as.list(bdy)
bdy_list <- bdy_list[-(n - 1L)]
bdy_list[[length(bdy_list)]] <- prev_expr[[3L]]
return(as.call(bdy_list))
}
}
return(bdy)
}

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

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

The nested conditional logic (lines 25-28) has multiple conditions combined in a single if statement, making it difficult to read and understand. Consider extracting this logic into a helper function or breaking it into multiple conditional checks with descriptive variable names.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

copilot, propose a suggested change.

Comment thread R/c-wrapper.R Outdated
Comment on lines +77 to +82
append(c_body) <- c(
glue("SEXP _ans = PROTECT(Rf_allocVector(VECSXP, {length(return_var_names_un)}));"),
imap(return_var_names_un, function(nm, i) {
glue("SET_VECTOR_ELT(_ans, {i-1}, {nm});")
}),
glue("SEXP _names = PROTECT(Rf_allocVector(STRSXP, {length(return_var_names_un)}));"),

Copilot AI Aug 26, 2025

Copy link

Choose a reason for hiding this comment

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

The expression length(return_var_names_un) is repeated twice. Consider extracting it to a variable like n_return_vars to improve readability and maintainability.

Suggested change
append(c_body) <- c(
glue("SEXP _ans = PROTECT(Rf_allocVector(VECSXP, {length(return_var_names_un)}));"),
imap(return_var_names_un, function(nm, i) {
glue("SET_VECTOR_ELT(_ans, {i-1}, {nm});")
}),
glue("SEXP _names = PROTECT(Rf_allocVector(STRSXP, {length(return_var_names_un)}));"),
n_return_vars <- length(return_var_names_un)
append(c_body) <- c(
glue("SEXP _ans = PROTECT(Rf_allocVector(VECSXP, {n_return_vars}));"),
imap(return_var_names_un, function(nm, i) {
glue("SET_VECTOR_ELT(_ans, {i-1}, {nm});")
}),
glue("SEXP _names = PROTECT(Rf_allocVector(STRSXP, {n_return_vars}));"),

Copilot uses AI. Check for mistakes.
@t-kalinowski

Copy link
Copy Markdown
Owner

@t-kalinowski, Thank you very much for the PR! It looks great at a glance. I'll review more thorougly and merge later in the week. Good work!

@mns-nordicals

mns-nordicals commented Aug 26, 2025

Copy link
Copy Markdown
Contributor Author

I saw that you were preparing for a new release, so I kind of hastily created a pull request in hopes of it making in to this release. So it is a bit raw.

Let me try to refactor the process-lang part and review some of the other parts before you review it again. I will create new commits in a day or two.

test file and added a failure test.
Removed some checkking in subroutine and c-wrapper

Changes to be committed:
	modified:   R/c-wrapper.R
	modified:   R/preprocess-lang.R
	modified:   R/subroutine.R
	deleted:    tests/testthat/test-multi-return.R
	new file:   tests/testthat/test-multiple-return.R
@mns-nordicals

Copy link
Copy Markdown
Contributor Author

@t-kalinowski I tried to refactor the code a bit and add some comments.

This pull request extends quickr by allowing it to return a list of vectors. This is achieved by:

  • Process the body of the function before translation. If we are using a list we ensure that the list call is the last expression
  • Then we adapt the rest of the pipeline to check for list call as the last expression
  • If we are returning a list we are marking each variable in the list as a return variable in new_fortran_subroutine
  • Then we modify manifest to reflect this and the c-wrapper is translating the fortran output to an R list

I notice that we are checking that the last expression is symbol in both ensure_last_expr_sym, new_fortran_subroutine and closure_return_var_names. I implemented a validate list in preprocess-lang - but i am not sure if you also think this is the place to validate and error.

@t-kalinowski t-kalinowski left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Very nice work! I think this is almost ready to merge. Just had some minor questions about how we handle named lists.

Comment thread R/c-wrapper.R Outdated
Comment thread tests/testthat/test-multiple-return.R
@mns-nordicals

mns-nordicals commented Aug 30, 2025

Copy link
Copy Markdown
Contributor Author

We are now much closer to R and we can return unnamed elements and custom names for elements. We still require that what is returned is a symbol like a <- 1; list(a) we don't allows list(a = 1). otherwise, I think we match R

  • list(a) -> unnamed list
  • list(abc = a) -> named list
  • list(a, abc = b, c) -> mixed named and unnamed list (names(list) -> c("", "abc", ""))

R/c-wrapper.R:

  1. updated closure_return_var_names() to always return a named vector
  2. extract symbol/value and names separeately in make_c_bridge()
  3. Only set R_NamesSymbol when at least one list element is named.
  4. Preserve provided names; leave unnamed elements as "" (no auto-fallback to symbol names). If all elements are unnamed, don’t attach a names attribute at all.

tests/testthat/test-multiple-return.R:

  1. Custom names preserved.
  2. No names added for fully unnamed returns.
  3. Mixed named and unnamed elements behave like base R.

@t-kalinowski

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment thread R/c-wrapper.R Outdated
Comment on lines +70 to +74
if (length(return_var_names) == 1L) {
if (n_protected > 0) {
append(c_body) <- glue("UNPROTECT({n_protected});")
}
append(c_body) <- glue("return {return_var_names};")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Preserve list semantics when only one value is returned

The bridge only wraps the return values into an R list when length(return_var_names) > 1. A function whose last expression is list(...) with a single element now falls through the single‑value branch and returns the bare SEXP instead of the list. For example, a quickr function ending with list(result = y) will compile to a function that returns y rather than list(result = y), so the compiled code no longer matches the R reference implementation. The branch should detect whether the user explicitly returned a list rather than relying solely on the number of elements.

Useful? React with 👍 / 👎.

Comment thread R/c-wrapper.R
Comment on lines +88 to +93
if (has_any_names) {
names_to_use <- provided_names
append(c_body) <- c(
glue("SEXP _names = PROTECT(Rf_allocVector(STRSXP, {length(return_var_values)}));"),
imap(names_to_use, function(nm, i) {
glue('SET_STRING_ELT(_names, {i-1}, Rf_mkChar("{nm}"));')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] Escape list element names before embedding in generated C

When named multiple return values are handled, the code writes each provided name directly into a C string literal (Rf_mkChar("{nm}")). If a caller supplies a name containing quotes or backslashes (list("a\"b" = y, z = z)), the generated C code becomes syntactically invalid or can miscompile. The names should be run through a C string escaping helper before interpolation.

Useful? React with 👍 / 👎.

@t-kalinowski

Copy link
Copy Markdown
Owner

@codex add tests for the single list edge case, show the test failing (by pushing), then implement the fix (in another commit).
For the non-syntatic names, just throw an error message that explains only syntatic names are valid.

@chatgpt-codex-connector

Copy link
Copy Markdown

@codex add tests for the single list edge case, show the test failing (by pushing), then implement the fix (in another commit).
For the non-syntatic names, just throw an error message that explains only syntatic names are valid.

For now, I can only help with PRs you've created.

@t-kalinowski

Copy link
Copy Markdown
Owner

@codex open a new PR into this branch.

@chatgpt-codex-connector

Copy link
Copy Markdown

@codex open a new PR into this branch.

For now, I can only help with PRs you've created.

…ts; validate non-syntactic names in return lists
@t-kalinowski

Copy link
Copy Markdown
Owner

@codex review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment thread R/c-wrapper.R Outdated
Comment on lines +41 to +43
return_var_names <- closure_return_var_names(closure)
return_vars <- mget(return_var_names, scope)
for (return_var in return_vars) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] De-duplicate return vars before emitting C allocations

The loop that prepares output buffers iterates over every entry returned by closure_return_var_names() without removing duplicates. If a function returns the same symbol twice (for example list(y, y)), the loop generates two identical declarations and PROTECT statements for y, which will fail compilation due to redeclared identifiers and mismatched protection counts. The code should allocate each return variable once and reuse it when packing the list.

Useful? React with 👍 / 👎.

Comment thread R/c-wrapper.R
Comment on lines +477 to +480
bad <- nzchar(nms) & make.names(nms) != nms
if (any(bad)) {
stop(
"only syntactic names are valid, encountered: ",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] Replace invalid paste0 call in non‑syntactic name error

When validating names for list returns, the error message uses paste0(nms[bad], sep = ", "), but paste0 does not accept a sep argument. Hitting this branch (e.g. returning list(a b = y)) raises an unused argument (sep = ...) before the intended stop() message, so callers see a confusing error unrelated to the actual problem. Use paste() or paste0(..., collapse = ", ") to list the offending names.

Useful? React with 👍 / 👎.

@t-kalinowski

Copy link
Copy Markdown
Owner

@codex review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment thread R/c-wrapper.R
Comment on lines +41 to +47
return_var_names <- closure_return_var_names(closure)
return_vars <- mget(return_var_names, scope)
for (return_var in return_vars) {
if (!return_var@name %in% closure_arg_names) {
return_var@modified <- TRUE
assign(return_var@name, return_var, scope)
append(c_body) <- return_var_c_defs(return_var, fsub@scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] Deduplicate return variables before emitting C defs

The loop generates return-variable declarations for every element returned by closure_return_var_names, including duplicates. When a function returns the same symbol more than once (e.g. list(y, y)), this code emits two identical return_var_c_defs blocks and the generated wrapper fails to compile with redefinition of ‘y__len_’/y. The set of return variables should be uniqued before generating the C definitions and PROTECT counts.

Useful? React with 👍 / 👎.

@t-kalinowski

Copy link
Copy Markdown
Owner

@codex review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

@t-kalinowski
t-kalinowski merged commit 599bd98 into t-kalinowski:main Sep 2, 2025
@mns-nordicals
mns-nordicals deleted the list_return_1 branch July 3, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to return multiple values

3 participants