Skip to content

fix: multi-line and conditional sec blocks in analytical models#302

Open
Siel wants to merge 2 commits intomainfrom
fix/sec-block-multiline-conditional
Open

fix: multi-line and conditional sec blocks in analytical models#302
Siel wants to merge 2 commits intomainfrom
fix/sec-block-multiline-conditional

Conversation

@Siel
Copy link
Copy Markdown
Member

@Siel Siel commented Mar 6, 2026

Summary

Fix sec block failing with "Result must be length 1" when it contains multiple statements or if-conditionals in analytical (template-based) models.

Example that was failing

mod <- PM_model$new(
  pri = list(
    Ka = ab(0.5, 6),
    Ke = ab(0.1, 1.5),
    v0 = ab(25, 120)
  ),
  cov = list(
    gender = interp("none"),
    wt = interp()
  ),
  sec = function() {
    v = v0 * wt
    if (gender < 1) { v = v0 * 0.8 }
  },
  eqn = function(){ two_comp_bolus },
  out = function(){ Y[1] = X[2]/v },
  err = list(proportional(5, c(0.1, 0.15, 0, 0)))
)

This also failed with multiple assignments (no conditionals):

sec = function() {
  v = v0 * wt
  cl = cl0 * wt
}

Root cause

deparse() on multi-line R expressions (e.g. if-blocks) returns a character vector longer than 1, which broke map_chr() calls that assumed single-element output in the transpiler and template detection code.

Changes

  • model_transpiler.R: collapse deparse() output in transpile_analytic_eqn()
  • mod_lib.R: same fix in get_found_model() for template detection
  • test-Examples.R: regression test for multi-line sec with conditionals

Thanks to Hector Julian Perez Madrid for finding this bug.

Thanks to Hector Julian Perez Madrid for finding this bug.
Copilot AI review requested due to automatic review settings March 6, 2026 16:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a failure in analytical (template-based) model handling where deparse() could return a multi-element character vector for multi-line expressions, which then broke purrr::map_chr()/template-detection logic (e.g., “Result must be length 1”).

Changes:

  • Collapse multi-line deparse() output into a single string when scanning EQN statements in transpile_analytic_eqn().
  • Apply the same deparse() collapsing approach in get_found_model() to make template detection robust to multi-line expressions.
  • Add a regression test covering an analytical model with a multi-line sec block containing an if conditional.
  • (Unrelated) Large-scale formatting changes in PMutilities.R.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
R/model_transpiler.R Collapses deparse() output during analytic EQN parsing to prevent map_chr() length errors.
R/mod_lib.R Collapses deparse() output during template detection; switches to map_lgl() for clearer boolean results.
tests/testthat/test-Examples.R Adds regression test for a multi-line sec block with conditionals in an analytical model.
R/PMutilities.R Contains extensive formatting-only changes unrelated to the stated PR purpose.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +213
eqn = function() {
two_comp_bolus
},
lag = function() {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The new regression test doesn’t currently exercise the code paths changed in get_found_model()/transpile_analytic_eqn() (i.e., deparse() returning a multi-element character vector for a multi-line expression inside the EQN/template-detection scan). As written, the EQN block only contains a single symbol, so the pre-fix code would likely not fail here. Consider adjusting/adding a case where the EQN block contains an if-block or another multi-line statement alongside the template name, so the test would fail without this PR and pass with it.

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +229
expect_s3_class(mod, "PM_model")
sec_code <- paste(deparse(mod$arg_list$sec), collapse = "\n")
expect_true(grepl("if (gender < 1)", sec_code, fixed = TRUE))
expect_true(grepl("v = v0 * wt", sec_code, fixed = TRUE))
})
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Since the bug is specific to analytical/template-based models, it would be good for this test to assert that the model was detected as Analytical (e.g., via mod$model_list$type) before checking sec contents. Otherwise, a future change that accidentally routes this model through the ODE path could let the test pass while no longer covering the intended behavior.

Copilot uses AI. Check for mistakes.
Comment thread R/PMutilities.R
Comment on lines +125 to +127
dmv_norm <- function(
x, mean = rep(0, p), sigma = diag(p), log = FALSE,
checkSymmetry = TRUE) {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This PR includes a very large reformatting-only change set in PMutilities.R that isn’t mentioned in the PR description and appears unrelated to the analytical SEC/EQN deparse fix. This makes the review noisy and increases merge-conflict risk; consider reverting these formatting changes or splitting them into a separate PR focused on styling/formatting.

Copilot uses AI. Check for mistakes.
@mhovd
Copy link
Copy Markdown
Collaborator

mhovd commented Mar 8, 2026

Apparently the CI is breaking because of spindle ?

Compiling spindle v0.2.5
error[E0597]: `task` does not live long enough
   --> /Users/runner/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/spindle-0.2.5/src/lib.rs:597:10
    |
594 |                     let task = AssertUnwindSafe(task);
    |                         ---- binding `task` declared here
595 |
596 |                     let task = |thread_id: usize, panic_slot: &AtomicPtr<PanicLoad>| match std::panic::catch_unwind(|| {
    |                                ----------------------------------------------------- value captured here
597 |                         ({ task }.0)(thread_id);
    |                            ^^^^ borrowed value does not live long enough
...
609 |                     let mut task = (&raw const task) as *const TaskInner;
    |                                                         ---------------- type annotation requires that `task` is borrowed for `'static`
...
635 |                 }
    |                 - `task` dropped here while still borrowed
    ```
    
    I will check if I can figure out why.

Comment thread R/PMutilities.R
blocks <- parseBlocks(model)

# check for reserved variable names
reserved <- c(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can probably revise this list, many of these are no longer really reserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

But that doesn't have to be in this PR.

Running cargo update to get the latest version of spindle
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.

3 participants