Skip to content

Commit 4d24465

Browse files
authored
ignoreIndexRanges argument for FORLOOP (#42)
* Add ignore argument to FORLOOP * Make sure simplifyIndices ignores any index ranges * Handle ignore on LHS and add better error handling * Add tests * Set environment when evaluating ignore argument * Update docs * More informative argument name
1 parent 2301f3e commit 4d24465

6 files changed

Lines changed: 147 additions & 8 deletions

File tree

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: nimbleMacros
2-
Version: 0.1.1.9003
3-
Date: 2026-06-09
2+
Version: 0.1.1.9004
3+
Date: 2026-06-10
44
Title: Macros Generating 'nimble' Code
55
Authors@R: c(person("Ken", "Kellner", email="contact@kenkellner.com",
66
role=c("cre","aut")),
@@ -23,4 +23,4 @@ URL: https://r-nimble.org
2323
BugReports: https://github.com/nimble-dev/nimbleMacros/issues
2424
Encoding: UTF-8
2525
VignetteBuilder: knitr
26-
RoxygenNote: 7.3.3
26+
Config/roxygen2/version: 8.0.0

R/FORLOOP.R

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#'
1212
#' @param code The right-hand side of a parameter declaration
1313
#' @param modelInfo Used internally by nimbleMacros; a list of model information such as constants and dimensions
14+
#' @param ignoreIndexRanges Index ranges to ignore when creating for loops; must be provided as a list of quoted code
1415
#' @param .env Used internally by nimbleMacros; the environment where the model was created
1516
#'
1617
#' @return NIMBLE code for a for loop or series of nested for loops.
@@ -27,7 +28,21 @@ NULL
2728

2829
#' @export
2930
FORLOOP <- nimble::buildMacro(
30-
function(code, modelInfo, .env){
31+
function(code, modelInfo, ignoreIndexRanges = list(), .env){
32+
# Super clunky way of extracting the ignore argument
33+
# seems to be necessary to do it this way given buildMacro options
34+
ignore <- eval(code[[length(code)]]$ignoreIndexRanges, envir = .env)
35+
# necessary for when ignore is not explicitly included in arguments
36+
if(is.null(ignore)) ignore <- list()
37+
# get rid of any ignore stuff in code
38+
code[[length(code)]]$ignoreIndexRanges <- NULL
39+
# Check ignore structure
40+
if(!is.list(ignore)) stop("ignoreIndexRanges argument must be a list", call.=FALSE)
41+
if(length(ignore) > 0){
42+
is_call <- sapply(ignore, is.call)
43+
if(!all(is_call)) stop("Some elements of ignoreIndexRanges are not quoted code/calls", call.=FALSE)
44+
}
45+
3146
# Remove FORLOOP from the code line
3247
code <- removeMacroCall(code)
3348
LHS <- getLHS(code)
@@ -45,11 +60,13 @@ function(code, modelInfo, .env){
4560

4661
# Subset only indexes with ranges
4762
idx_sub <- idx[has_range]
63+
# Remove indices in ignore
64+
idx_sub <- idx_sub[! idx_sub %in% ignore]
4865
# Create new index letters to represent those ranges in the for loops
4966
idx_letters <- lapply(1:length(idx_sub), function(i) modelInfo$indexCreator())
5067
idx_letters <- lapply(idx_letters, as.name)
5168
# Replace the ranges with the new corresponding letters
52-
code <- replaceDeclarationIndexRanges(code, idx_letters)
69+
code <- replaceDeclarationIndexRanges(code, idx_letters, ignore)
5370
# Then insert the ranges into the for loop structure
5471
idx_sub <- replaceRanges(idx_sub, idx_letters)
5572
# So e.g.
@@ -168,13 +185,16 @@ removeIndexAdjustments <- function(idx){
168185

169186
# Check if index range on both sides of declaration are the same
170187
# Specifically, all indices on RHS have to be present in LHS
171-
hasMatchingIndexRanges <- function (LHS, RHS){
188+
# ignoreIndexRanges must be a list of quoted code e.g. list(quote(1:n))
189+
hasMatchingIndexRanges <- function (LHS, RHS, ignoreIndexRanges = list()){
172190
idx_LHS <- extractIndices(LHS)
173191
idx_LHS <- idx_LHS[isIndexRange(idx_LHS)]
174192
idx_LHS <- lapply(idx_LHS, removeIndexAdjustments)
175193
idx_RHS <- extractAllIndices(RHS)
176194
idx_RHS <- idx_RHS[isIndexRange(idx_RHS)]
177195
idx_RHS <- lapply(idx_RHS, removeIndexAdjustments)
196+
idx_RHS <- idx_RHS[!idx_RHS %in% ignoreIndexRanges]
197+
if(length(idx_RHS) == 0) return(TRUE)
178198
all(idx_RHS %in% idx_LHS)
179199
}
180200

@@ -223,13 +243,16 @@ recursiveReplaceIndex <- function(code, old_idx, new_idx){
223243
}
224244

225245
# Replace all indices on both sides LHS/RHS
226-
replaceDeclarationIndexRanges <- function(code, new_idx_list){
246+
# ignoreIndexRanges, if provided, must be a list of quoted code
247+
# e.g. list(quote(1:n), quote(1:3))
248+
replaceDeclarationIndexRanges <- function(code, new_idx_list, ignoreIndexRanges = list()){
227249
LHS <- getLHS(code)
228250
RHS <- getRHS(code)
229251
op <- code[[1]]
230-
if(!hasMatchingIndexRanges(LHS, RHS)) stop("Index ranges must match")
252+
if(!hasMatchingIndexRanges(LHS, RHS, ignoreIndexRanges)) stop("Index ranges must match")
231253
idx <- extractAllIndices(LHS)
232254
idx <- idx[isIndexRange(idx)]
255+
idx <- idx[! idx %in% ignoreIndexRanges]
233256
if(length(idx) == 0) return(code)
234257
nidx <- length(idx)
235258
for (i in 1:nidx){

R/utilities.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ simplifyIndices <- function(code, new_indices){
195195
if(is.name(x) | is.symbol(x)) return(x)
196196
if(x[[1]] == "for"){
197197
unique_idx <- unique(extractAllIndices(x))
198+
# Ignore any remaining index *ranges*
199+
unique_idx <- unique_idx[sapply(unique_idx, is.name)]
198200
# Ignore blank indices as in x[i,]
199201
unique_idx <- unique_idx[sapply(unique_idx, function(x) x!="")]
200202
if(length(unique_idx) > length(new_indices)){

man/FORLOOP.Rd

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test_FORLOOP.R

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,87 @@ test_that("FORLOOP", {
322322

323323
nimbleOptions(enableMacroComments=comments_on)
324324
})
325+
326+
test_that("ignoreIndexRanges argument works", {
327+
comments_on <- nimbleOptions()$enableMacroComments
328+
nimbleOptions(enableMacroComments=FALSE)
329+
330+
# Check error when index on RHS not on LHS, and not ignored
331+
const <- list(y=rnorm(2), x=matrix(rnorm(6), 2, 3), n=2)
332+
modInfo <- list(constants = const, indexCreator = nimble:::labelFunctionCreator("i"))
333+
code <- quote(y[1:n] <- FORLOOP(sum(x[1:n, 1:3])))
334+
expect_error(FORLOOP$process(code, modelInfo=modInfo, .env=environment()),
335+
"Index ranges must match")
336+
337+
# Check malformed ignore arguments
338+
code <- quote(y[1:n] <- FORLOOP(sum(x[1:n, 1:3]), ignoreIndexRanges=1:3))
339+
expect_error(FORLOOP$process(code, modelInfo=modInfo, .env=environment())$code,
340+
"ignoreIndexRanges argument must be a list")
341+
342+
code <- quote(y[1:n] <- FORLOOP(sum(x[1:n, 1:3]), ignoreIndexRanges=list(1:3, 2)))
343+
expect_error(FORLOOP$process(code, modelInfo=modInfo, .env=environment())$code,
344+
"Some elements of ignoreIndexRanges are not quoted code/calls")
345+
346+
# Simple loop with one ignored range
347+
code <- nimbleCode({
348+
y[1:n] <- FORLOOP(sum(x[1:n, 1:3]), ignoreIndexRanges = list(quote(1:3)))
349+
})
350+
mod <- nimbleModel(code, constants=const)
351+
expect_equal(
352+
mod$getCode(),
353+
quote({
354+
for (i_1 in 1:n) {
355+
y[i_1] <- sum(x[i_1, 1:3])
356+
}
357+
})
358+
)
359+
360+
# Nested loops
361+
const <- list(y=matrix(rnorm(4), 2, 2), x=array(rnorm(12), c(2,2,3)), n=2)
362+
code <- nimbleCode({
363+
y[1:n, 1:2] <- FORLOOP(sum(x[1:n, 1:2, 1:3]), ignoreIndexRanges = list(quote(1:3)))
364+
})
365+
mod <- nimbleModel(code, constants=const)
366+
expect_equal(
367+
mod$getCode(),
368+
quote({
369+
for (i_1 in 1:n) {
370+
for (i_2 in 1:2) {
371+
y[i_1, i_2] <- sum(x[i_1, i_2, 1:3])
372+
}
373+
}
374+
})
375+
)
376+
377+
# Multiple ignores
378+
const <- list(y=rnorm(2), x=array(rnorm(12), c(2,2,3)), n=2)
379+
code <- nimbleCode({
380+
y[1:2] <- FORLOOP(sum(x[1:n, 1:2, 1:3]), ignoreIndexRanges = list(quote(1:3), quote(1:n)))
381+
})
382+
mod <- nimbleModel(code, constants=const)
383+
expect_equal(
384+
mod$getCode(),
385+
quote({
386+
for (i_1 in 1:2) {
387+
y[i_1] <- sum(x[1:n, i_1, 1:3])
388+
}
389+
})
390+
)
391+
392+
# Ignore on LHS
393+
const <- list(y=matrix(rnorm(4), 2, 2), x=matrix(rnorm(4), 2, 2), n=2)
394+
code <- nimbleCode({
395+
y[1:n, 1:2] <- FORLOOP(x[1:n, 1:2], ignoreIndexRanges = list(quote(1:n)))
396+
})
397+
mod <- nimbleModel(code, constants=const)
398+
expect_equal(
399+
mod$getCode(),
400+
quote({
401+
for (i_1 in 1:2){
402+
y[1:n, i_1] <- x[1:n, i_1]
403+
}
404+
})
405+
)
406+
407+
nimbleOptions(enableMacroComments=comments_on)
408+
})

tests/testthat/test_simplifyForLoops.R

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,31 @@ test_that("Blank index slot is ignored", {
152152
)
153153
})
154154

155+
test_that("Remaining index ranges are ignored", {
156+
# Index ranges could remain in loop code based on use of ignore argument
157+
# make sure simplifyForLoops doesn't replace these with a new index
158+
test <- nimbleCode({
159+
for (i_1 in 1:n){
160+
for (i_2 in 1:2){
161+
y[i_1,i_2] <- sum(x[i_1,i_2,1:m])
162+
}
163+
}
164+
165+
for(i_1 in 1:n){
166+
z[i_1] <- sum(a[1:3])
167+
}
168+
})
169+
170+
out <- simplifyForLoops(test)
171+
172+
expect_equal(out,
173+
quote({
174+
for (i in 1:n) {
175+
for (j in 1:2) {
176+
y[i, j] <- sum(x[i, j, 1:m])
177+
}
178+
z[i] <- sum(a[1:3])
179+
}
180+
})
181+
)
182+
})

0 commit comments

Comments
 (0)