Skip to content

tp_compile() silently returns a phantom model when tpplc fails with exit code != 1 #28

Description

@maribraga

Environment

  • treepplr 0.14.0
  • tpplc 0.4 (aarch64-darwin)

Summary

treepplr:::compilation() treats only exit code 1 as a compilation failure:

res <- system(paste0("LD_LIBRARY_PATH= ", command), intern = FALSE)
if (res == 1L) {
    stop("Compilation failed")
}
return(output_path)

But tpplc uses more than one nonzero exit code. Measured on 0.4:

failure mode exit code executable produced caught by treepplr
unknown symbol (symbolize error) 1 no yes
syntax error 1 no yes
invalid CLI option 1 no yes
fatal error, e.g. a missing import file 2 no NO
success 0 yes n/a

So when tpplc dies with Fatal error: exception Sys_error(...) — which is what you get from an unresolvable importtp_compile() returns a compiled_model_Template whose exe_path points at a file that was never created.

Reproducer

library(treepplr)
writeLines(c('import "does-not-exist.tppl"',
             'model function m() => Real { return 1.; }'), "/tmp/f1.tppl")
writeLines("{}", "/tmp/f1data.json")

m <- tp_compile("/tmp/f1.tppl", method = "is")
#> Fatal error: exception Sys_error("/tmp/does-not-exist.tppl: No such file or directory")
#  ... and yet:
class(m)                  #> "compiled_model_Template"
file.exists(m$exe_path)   #> FALSE

tp_run(m, "/tmp/f1data.json")
#> sh: /.../<hash>.exe: No such file or directory
#> Warning message:
#> In system(command) : error in running command
#> list()

Why this is worse than a confusing error

The failure is silent in both stages:

  1. tp_compile() reports success for a model that does not exist.
  2. tp_run() on the phantom executable raises only a warning, not an error, and returns list() — an empty list.

Downstream, out[[1]]$normConst then fails with a subscript error far from the real cause, and any code that iterates over the result (for (s in out) ..., lapply(out, ...)) silently does nothing at all and reports no problem.

This is not hypothetical. I hit it while porting a model whose import paths were stale relative to the installed compiler, and briefly believed I had compiled and run something I had not. Any workflow that compiles several model variants in one session and compares their numeric output is exposed to this: one variant fails to build, and the comparison quietly proceeds with a missing or stale result. My scripts now defensively assert file.exists(sampler$exe_path) after every tp_compile(), which should not be the caller's job.

Suggested fix

Two independent guards, both cheap:

res <- system(paste0("LD_LIBRARY_PATH= ", command), intern = FALSE)
if (res != 0L) {
  stop("Compilation failed (tpplc exit status ", res, "): ", command)
}
if (!file.exists(output_path)) {
  stop("Compilation reported success but produced no executable at ", output_path)
}

Checking res != 0L covers every current and future nonzero code; the file.exists() check is belt-and-braces in case tpplc ever exits 0 without emitting a binary. Including the exit status and the command in the error message would also help a lot here — see also #24 on surfacing tpplc's own stderr rather than a bare "Compilation failed".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions