diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index e1f2b01..016f890 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -102,7 +102,7 @@ jobs: id: set-matrix shell: bash run: | - json=$(bash rcp/tests/list-tests.sh | tr ' ' '\n' | jq -R . | jq -sc '{"test": .}') + json=$(find rcp/tests -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort | jq -R . | jq -sc '{"test": .}') echo "Discovered tests: $json" echo "matrix=$json" >> "$GITHUB_OUTPUT" @@ -112,7 +112,6 @@ jobs: needs: build-images strategy: fail-fast: false - max-parallel: 1 matrix: ${{ fromJson(needs.build-images.outputs.test-matrix) }} permissions: contents: read diff --git a/.gitignore b/.gitignore index d4fb526..b65bb82 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ perf.data /rcp/tests/perf/*.old /rcp/tests/perf/*.data /rcp/tests/perf/profile.json.gz +/rcp/.install-stamp +/rcp/.install-stamp.cfg diff --git a/external/rsh b/external/rsh index 71633e0..6276fe0 160000 --- a/external/rsh +++ b/external/rsh @@ -1 +1 @@ -Subproject commit 71633e0fb17f48b7b94f3974535411090f88f3c1 +Subproject commit 6276fe06771221a90abd3eb0f7a7ff9c1e857678 diff --git a/rcp/Makefile b/rcp/Makefile index 8f90a84..d0b18b2 100644 --- a/rcp/Makefile +++ b/rcp/Makefile @@ -1,45 +1,23 @@ include common.mk + .PHONY: all all: install -.PHONY: check-toolchain -check-toolchain: - @command -v $(CC) >/dev/null 2>&1 || (echo "Missing compiler: $(CC)"; exit 1) - @command -v $(CXX) >/dev/null 2>&1 || (echo "Missing compiler: $(CXX)"; exit 1) - @cc_ver=`$(CC) -dumpfullversion -dumpversion | cut -d. -f1`; \ - if [ "$$cc_ver" != "14" ]; then \ - echo "Expected GCC major version 14 for $(CC), got $$cc_ver"; \ - exit 1; \ - fi - @cxx_ver=`$(CXX) -dumpfullversion -dumpversion | cut -d. -f1`; \ - if [ "$$cxx_ver" != "14" ]; then \ - echo "Expected G++ major version 14 for $(CXX), got $$cxx_ver"; \ - exit 1; \ - fi - @printf 'int main(void){return 0;}\n' | $(CC) $(C_STD_FLAG) -x c -fsyntax-only - - @printf '#include \nint main(){return 0;}\n' | $(CXX) $(CXX_STD_FLAG) -x c++ -fsyntax-only - - .PHONY: install -install: check-toolchain - MAKEFLAGS="$(MAKEFLAGS) CC=$(CC) CXX=$(CXX)" $(R) CMD INSTALL . +BEAR := $(shell command -v bear 2>/dev/null) +BEAR := $(if $(BEAR),$(BEAR) --) + +install: + $(BEAR) $(R) CMD INSTALL . .PHONY: clean clean: $(MAKE) -C src -f Makevars clean + rm -f compile_commands.json .PHONY: test -test: check-toolchain - $(MAKE) clean && $(MAKE) install DEBUG=1 RCP_COMPILE_PROMISES=1 - $(MAKE) -C tests test; \ - status=$$?; \ - $(MAKE) clean \ - exit $$status - -.PHONY: test-functional -test-functional: install - $(MAKE) -C tests/smoketest test - $(MAKE) -C tests/types test - $(MAKE) -C tests/gdb-jit test +test: install + $(MAKE) -C tests test .PHONY: run run: install @@ -53,20 +31,13 @@ debug: install format: find src -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" \) -exec clang-format -i {} + -.PHONY: setup -setup: check-toolchain - $(R) --quiet -e 'install.packages("microbenchmark", repos="https://cloud.r-project.org")' - BENCH_ITER ?= 15 BENCH_PARALLEL ?= 1 BENCH_OUT_DIR ?= .PHONY: benchmark -benchmark: check-toolchain +benchmark: @$(call ensure_microbenchmark_installed) @RSH_HOME=$(RSH_HOME) R_HOME=$(R_HOME) ./inst/benchmarks/run-benchmarks.sh \ --runs $(BENCH_ITER) --parallel $(BENCH_PARALLEL) \ $(if $(BENCH_OUT_DIR),--output $(BENCH_OUT_DIR)) - -compile_commands.json: - bear -- $(MAKE) clean install diff --git a/rcp/R/compile.R b/rcp/R/compile.R index aadeddc..930e988 100644 --- a/rcp/R/compile.R +++ b/rcp/R/compile.R @@ -1,7 +1,24 @@ +.rcp_banner <- function() { + info <- .Call("C_rcp_build_info") + ver <- utils::packageVersion("rcp") + flags <- c( + if (info$compile_promises) "promises", + if (nzchar(Sys.getenv("RCP_DUMP_DIR"))) paste0("dump:", Sys.getenv("RCP_DUMP_DIR")), + if (nzchar(Sys.getenv("RCP_GDB_JIT"))) "gdb", + if (nzchar(Sys.getenv("RCP_PERF_JIT"))) "perf" + ) + flag_str <- if (length(flags)) paste0(" [", paste(flags, collapse = ", "), "]") else "" + packageStartupMessage(sprintf("rcp %s (%s)%s", ver, info$git_commit, flag_str)) +} + .onLoad <- function(libname, pkgname) { .Call("rcp_init"); } +.onAttach <- function(libname, pkgname) { + .rcp_banner() +} + #' Compile a function #' #' This function compiles another function with optional settings. @@ -46,7 +63,7 @@ rcp_jit_disable <- function() { #' @return A list with counts of successfully compiled and failed functions #' @export rcp_cmppkg <- function(package) { - .Call(C_rcp_cmppkg, package) + invisible(.Call(C_rcp_cmppkg, package)) } #' Get profiling data from RCP diff --git a/rcp/code.R b/rcp/code.R deleted file mode 100644 index 366d4ab..0000000 --- a/rcp/code.R +++ /dev/null @@ -1,84 +0,0 @@ -options(rcp.cmpfun.entry_exit_hooks = TRUE) -library(rcp) -fib <- function(x) { - if (x == 0) 0 - else if (x == 1) 1 - else fib(x-2) + fib(x-1) -} - -fib = rcp::rcp_cmpfun(fib, list(name="fib")) -fib(10) -print(rcp::rcp_get_types_df("fib")) - -library(rcp) -test <- function(x) { - if (x == 0) x=10 - else x=11 - x -} - -test =rcp::rcp_cmpfun(test); -test(1) - -exec <- function(x) { - 1 -} - - -tmp = rcp::rcp_cmpfun(exec) - - - -exec <- function(x) { -repeat { - next - } -} - -library(rcp) -f <- function(x) { - y <- x + 1 - if(y > 0){ - z <- x - 1 - } - else { - z <- x + 1 - } - y <- z / y - z -} -f = rcp::rcp_cmpfun(f, list(name="f")) -f(14) -print(rcp::rcp_get_types_df("f")) - -library(rcp) - -g <- function(x, y) { - cat(x, y, "\n") - x -} -g = rcp::rcp_cmpfun(g, list(name = "g")) -g(34, "hello") -g(1L, "world!") -g("Nope", 456) -print(rcp::rcp_get_types_df("g")) - -library(rcp) -h <- function(a, ...) { - cat(a, ..., "\n") -} -h = rcp::rcp_cmpfun(h, list(name = "h")) -h(1, "hello") -h("world", 4, "three") -h(4L, t=89) -print(rcp::rcp_get_types_df("h")) - -library(rcp) -p <- function(x, y) { - cat(x, y, "\n") - y -} -p <- rcp::rcp_cmpfun(p, list(name = "p")) -p(1, "hello") -p(y=3, x="world") -print(rcp::rcp_get_types_df("p")) \ No newline at end of file diff --git a/rcp/src/Makevars b/rcp/src/Makevars index f014f74..0b30d26 100644 --- a/rcp/src/Makevars +++ b/rcp/src/Makevars @@ -1,5 +1,8 @@ include ../common.mk +RCP_GIT_COMMIT := $(shell git -C $(ROOT_DIR) rev-parse --short HEAD 2>/dev/null || echo NA) +PKG_CFLAGS += -DRCP_GIT_COMMIT=\"$(RCP_GIT_COMMIT)\" + OPENMP ?= 0 ifneq ($(OPENMP),0) diff --git a/rcp/src/compile.c b/rcp/src/compile.c index 8009967..795b59b 100644 --- a/rcp/src/compile.c +++ b/rcp/src/compile.c @@ -767,6 +767,7 @@ static int can_fallthrough_from_opcode(RCP_BC_OPCODES opcode) switch (opcode) { case (RETURN_BCOP): + case (RETURNJMP_BCOP): case (GOTO_BCOP): case (STARTFOR_BCOP): case (SWITCH_BCOP): @@ -1075,6 +1076,19 @@ static void peephole_goto(int bytecode[], int bytecode_size, SEXP *constpool) } } +static void dump_compiled_binary(const char *dump_dir, const char *name, const uint8_t *executable, size_t size) +{ + char dump_path[512]; + snprintf(dump_path, sizeof(dump_path), "%s/%s.o", dump_dir, name); + FILE *fp = fopen(dump_path, "wb"); + if (fp) + { + fwrite(executable, 1, size, fp); + fclose(fp); + fprintf(stderr, "RCP: wrote binary to %s (%zu bytes)\n", dump_path, size); + } +} + typedef struct PluginStencil { int pos; @@ -1507,11 +1521,20 @@ static rcp_exec_ptrs copy_patch_internal(int bytecode[], int bytecode_size, res.eh_frame_data = eh_frame_data; if (rcp_gdb_jit_enabled) + { res.jit_entry = gdb_jit_register(name, executable, insts_size, inst_addrs_packed, count_opcodes, instruction_stencils); + } else + { res.jit_entry = NULL; + const char *dump_dir = getenv("RCP_DUMP_DIR"); + if (dump_dir) + { + dump_compiled_binary(dump_dir, name, executable, insts_size); + } + } if (rcp_perf_jit_enabled) { @@ -2104,6 +2127,64 @@ SEXP C_rcp_is_compiled(SEXP closure) return Rf_ScalarLogical(TRUE); } +static const char *guess_closure_name(SEXP f) +{ + SEXP env = CLOENV(f); + if (env == R_EmptyEnv || env == R_NilValue) + return NULL; + + SEXP names = PROTECT(R_lsInternal3(env, TRUE, FALSE)); + int n = LENGTH(names); + const char *sym_name = NULL; + + for (int i = 0; i < n; i++) + { + const char *s = CHAR(STRING_ELT(names, i)); + SEXP val = Rf_findVarInFrame(env, Rf_install(s)); + if (val == f) + { + sym_name = s; + break; + } + } + UNPROTECT(1); // names + + if (sym_name == NULL) + return NULL; + + const char *prefix = NULL; + int prefix_len = 0; + char env_buf[32]; + + if (R_IsNamespaceEnv(env)) + { + prefix = CHAR(STRING_ELT(R_NamespaceEnvSpec(env), 0)); + prefix_len = strlen(prefix) + 2; // "pkg::" + } + else if (env == R_GlobalEnv) + { + prefix = NULL; + prefix_len = 0; + } + else + { + snprintf(env_buf, sizeof(env_buf), "__%p", (void *)env); + prefix = env_buf; + prefix_len = strlen(env_buf) + 2; // "__0x...::sym" + } + + int sym_len = strlen(sym_name); + int total = (prefix ? prefix_len : 0) + sym_len + 1; + char *result = R_alloc(total, 1); + + if (prefix && env != R_GlobalEnv) + snprintf(result, total, "%s::%s", prefix, sym_name); + else + snprintf(result, total, "%s", sym_name); + + return result; +} + SEXP C_rcp_cmpfun(SEXP f, SEXP options) { DEBUG_PRINT("Starting to JIT a function...\n"); @@ -2208,6 +2289,15 @@ SEXP C_rcp_cmpfun(SEXP f, SEXP options) } } + if (strcmp(name, "") == 0) + { + const char *guessed = guess_closure_name(f); + if (guessed != NULL) + { + name = guessed; + } + } + DEBUG_PRINT("Compiling %s to bytecode...\n", name); SEXP compiled; @@ -2731,7 +2821,7 @@ SEXP C_rcp_get_types(void) SET_STRING_ELT(args_names, k, PRINTNAME(trace->argument_names[k])); else { - char name_buf[16]; + char name_buf[32]; snprintf(name_buf, sizeof(name_buf), "arg%zu", k + 1); SET_STRING_ELT(args_names, k, Rf_mkChar(name_buf)); } @@ -2911,7 +3001,7 @@ SEXP C_rcp_get_types_df(SEXP func_name_sexp) } else { - char name_buf[16]; + char name_buf[32]; snprintf(name_buf, sizeof(name_buf), "arg%zu", c + 1); SET_STRING_ELT(col_names, c, Rf_mkChar(name_buf)); } @@ -3090,6 +3180,24 @@ SEXP rcp_init(void) return R_NilValue; } +#ifndef RCP_GIT_COMMIT +#define RCP_GIT_COMMIT "NA" +#endif + +SEXP C_rcp_build_info(void) +{ + const char *names[] = {"git_commit", "compile_promises", ""}; + SEXP info = PROTECT(Rf_mkNamed(VECSXP, names)); + SET_VECTOR_ELT(info, 0, mkString(RCP_GIT_COMMIT)); +#ifdef RCP_COMPILE_PROMISES + SET_VECTOR_ELT(info, 1, ScalarLogical(1)); +#else + SET_VECTOR_ELT(info, 1, ScalarLogical(0)); +#endif + UNPROTECT(1); + return info; +} + void rcp_destr(void) { if (rcp_perf_jit_enabled) diff --git a/rcp/src/gdb_jit.c b/rcp/src/gdb_jit.c index c703065..2399c52 100644 --- a/rcp/src/gdb_jit.c +++ b/rcp/src/gdb_jit.c @@ -836,10 +836,37 @@ struct jit_code_entry *gdb_jit_register(const char *func_name, void *code_addr, FILE *fp = fopen(dump_path, "wb"); if (fp) { - fwrite(elf, 1, elf_size, fp); + // Create a copy with embedded code bytes so the ELF is + // self-contained for standalone inspection (e.g. gdb f.o). + // The in-memory ELF uses SHT_NOBITS for .text since GDB's + // JIT interface reads code from process memory directly. + size_t text_offset = (elf_size + 15) & ~15; // align to 16 + size_t dump_size = text_offset + code_size; + uint8_t *dump_elf = malloc(dump_size); + if (dump_elf) + { + memcpy(dump_elf, elf, elf_size); + memcpy(dump_elf + text_offset, code_addr, code_size); + + // Patch .text: SHT_NOBITS -> SHT_PROGBITS + Elf64_Ehdr *dehdr = (Elf64_Ehdr *)dump_elf; + Elf64_Shdr *dshdrs = + (Elf64_Shdr *)(dump_elf + dehdr->e_shoff); + dshdrs[SEC_TEXT].sh_type = SHT_PROGBITS; + dshdrs[SEC_TEXT].sh_offset = text_offset; + + // Patch program header so the segment maps file data + Elf64_Phdr *dphdr = + (Elf64_Phdr *)(dump_elf + dehdr->e_phoff); + dphdr->p_offset = text_offset; + dphdr->p_filesz = code_size; + + fwrite(dump_elf, 1, dump_size, fp); + free(dump_elf); + } fclose(fp); fprintf(stderr, "DEBUG: wrote JIT ELF to %s (%zu bytes)\n", - dump_path, elf_size); + dump_path, dump_size); } } diff --git a/rcp/src/rcp_init.c b/rcp/src/rcp_init.c index c5b25a2..8de30bf 100644 --- a/rcp/src/rcp_init.c +++ b/rcp/src/rcp_init.c @@ -18,6 +18,7 @@ extern SEXP C_rcp_gdb_jit_support(void); extern SEXP C_rcp_perf_jit_support(void); extern SEXP rcp_init(void); extern void rcp_destr(void); +extern SEXP C_rcp_build_info(void); extern SEXP __rcp_throw_exception(void); extern SEXP __rcp_test_catch(SEXP expr, SEXP env); @@ -36,6 +37,7 @@ static const R_CallMethodDef CallEntries[] = { {"rcp_dwarf_support", (DL_FUNC)&C_rcp_dwarf_support, 0}, {"rcp_gdb_jit_support", (DL_FUNC)&C_rcp_gdb_jit_support, 0}, {"rcp_perf_jit_support", (DL_FUNC)&C_rcp_perf_jit_support, 0}, + {"C_rcp_build_info", (DL_FUNC)&C_rcp_build_info, 0}, {"rcp_init", (DL_FUNC)&rcp_init, 0}, {"__rcp_throw_exception", (DL_FUNC)&__rcp_throw_exception, 0}, {"__rcp_test_catch", (DL_FUNC)&__rcp_test_catch, 2}, diff --git a/rcp/src/stencils/Makefile b/rcp/src/stencils/Makefile index d2b9894..ecfe299 100644 --- a/rcp/src/stencils/Makefile +++ b/rcp/src/stencils/Makefile @@ -49,7 +49,9 @@ OBJ = $(notdir $(STENCILS_OBJ)) .PHONY: all all: $(OBJ) -$(OBJ): $(OBJ:.o=.c) +RUNTIME_H := $(RSH_HOME)/src/bc2c/runtime.h + +$(OBJ): $(OBJ:.o=.c) $(RUNTIME_H) $(CC) $(CFLAGS) -c $< -o $@ .PHONY: clean diff --git a/rcp/tests/Makefile b/rcp/tests/Makefile index f3a6806..23c304a 100644 --- a/rcp/tests/Makefile +++ b/rcp/tests/Makefile @@ -1,4 +1,4 @@ -SUBDIRS = $(shell bash list-tests.sh) +SUBDIRS := $(wildcard */) .PHONY: all test clean $(SUBDIRS) diff --git a/rcp/tests/gdb-jit/Makefile b/rcp/tests/gdb-jit/Makefile index 9ccda08..92d8eaa 100644 --- a/rcp/tests/gdb-jit/Makefile +++ b/rcp/tests/gdb-jit/Makefile @@ -2,18 +2,14 @@ include ../../common.mk SUBDIRS = gdb-next gdb-recursion -.PHONY: all test clean re-record +.PHONY: all test clean all: test test: @RCP_GDB_JIT=1 R_HOME=$(R_HOME) python3 ./run-gdb-tests.py $(SUBDIRS) -re-record: - @RCP_GDB_JIT=1 R_HOME=$(R_HOME) python3 ./run-gdb-tests.py --update $(SUBDIRS) - clean: for dir in $(SUBDIRS); do \ rm -f $$dir/actual.log; \ done - diff --git a/rcp/tests/gdb-jit/gdb-next/expected.log b/rcp/tests/gdb-jit/gdb-next/expected.log deleted file mode 100644 index 63a4323..0000000 --- a/rcp/tests/gdb-jit/gdb-next/expected.log +++ /dev/null @@ -1,49 +0,0 @@ -Breakpoint 1 (__jit_debug_register_code) pending. -> library(rcp) -> -> # Minimal function: f(x) = x + 1 -> # Expected Bytecode: -> # 1. GETVAR x -> # 2. LDCONST 1 -> # 3. ADD -> # 4. RETURN -> f <- function(x) x + 1 -> -> cat("Compiling minimal function...\n") -Compiling minimal function... -> f_jit <- rcp::rcp_cmpfun(f, list(name="f_jit")) - -Breakpoint 1, __jit_debug_register_code () at gdb_jit.c:XXX -53 { -gdb_jit_register (func_name=func_name@entry=0xADDR "f_jit", code_addr=code_addr@entry=0xADDR , code_size=code_size@entry=1848, inst_addrs=inst_addrs@entry=0xADDR, instruction_count=instruction_count@entry=4, stencils=stencils@entry=0xADDR) at gdb_jit.c:XXX -871 __jit_debug_descriptor.action_flag = JIT_NOACTION; -Breakpoint 2 at 0xADDR: file /tmp/rcp_jit_XXXXXX/f_jit.S, line 1. -> -> cat("Executing function...\n") -Executing function... -> res <- f_jit(10) - -Breakpoint 2, f_jit (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/f_jit.S:1 -1 GETVAR_OP_ -#0 f_jit (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/f_jit.S:1 -#1 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#2 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#3 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#4 0xADDR in R_execClosure (call=call@entry=0xADDR, newrho=newrho@entry=0xADDR, sysparent=, rho=rho@entry=0xADDR, arglist=arglist@entry=0xADDR, op=op@entry=0xADDR) at eval.c:XXX -#5 0xADDR in applyClosure_core (call=call@entry=0xADDR, op=op@entry=0xADDR, arglist=0xADDR, rho=rho@entry=0xADDR, suppliedvars=, unpromise=unpromise@entry=TRUE) at eval.c:XXX -#6 0xADDR in Rf_applyClosure (call=0xADDR, op=0xADDR, arglist=, rho=0xADDR, suppliedvars=, unpromise=TRUE) at eval.c:XXX -#7 Rf_eval (e=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#8 0xADDR in do_set (call=0xADDR, op=0xADDR, args=0xADDR, rho=0xADDR) at Rinlinedfuns.h:XXX -#9 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#10 0xADDR in Rf_ReplIteration (rho=rho@entry=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0, state=state@entry=0xADDR) at main.c:XXX -#11 0xADDR in R_ReplConsole (rho=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0) at main.c:XXX -#12 0xADDR in run_Rmainloop () at main.c:XXX -#13 0xADDR in Rf_mainloop () at main.c:XXX -#14 0xADDR in main (ac=, av=) at Rmain.c:XXX -2 LDCONST_OP_DBL -Stack Top after GETVAR (should be 10):dbl: 10.000000 -3 ADD_OP_ -Stack Top after LDCONST (should be 1):dbl: 1.000000 -4 RETURN_OP_ -Stack Top after ADD (should be 11):dbl: 11.000000 -Function "__jit_debug_register_code" not defined. diff --git a/rcp/tests/gdb-jit/gdb-next/test.gdb b/rcp/tests/gdb-jit/gdb-next/test.gdb index 967d891..74c5d66 100644 --- a/rcp/tests/gdb-jit/gdb-next/test.gdb +++ b/rcp/tests/gdb-jit/gdb-next/test.gdb @@ -11,21 +11,23 @@ break f_jit continue # Line 1 (GETVAR) - we just hit the breakpoint +echo ===BT1_START===\n bt +echo ===BT1_END===\n next # Line 2 (LDCONST) - stepped over GETVAR -echo Stack Top after GETVAR (should be 10): +echo Stack Top after GETVAR (should be 10):\n call rcp_print_stack_val((void*)((char*)stack - 16)) next # Line 3 (ADD) - stepped over LDCONST -echo Stack Top after LDCONST (should be 1): +echo Stack Top after LDCONST (should be 1):\n call rcp_print_stack_val((void*)((char*)stack - 16)) next # Line 4 (RETURN) - stepped over ADD -echo Stack Top after ADD (should be 11): +echo Stack Top after ADD (should be 11):\n call rcp_print_stack_val((void*)((char*)stack - 16)) quit diff --git a/rcp/tests/gdb-jit/gdb-recursion/expected.log b/rcp/tests/gdb-jit/gdb-recursion/expected.log deleted file mode 100644 index 26e3551..0000000 --- a/rcp/tests/gdb-jit/gdb-recursion/expected.log +++ /dev/null @@ -1,99 +0,0 @@ -Breakpoint 1 (__jit_debug_register_code) pending. -> library(rcp) -> -> # Factorial: fac(3) -> fac(2) -> fac(1) -> returns 6 -> fac <- function(x) { -+ if (x <= 1) { -+ return(1) -+ } else { -+ return(x * fac(x - 1)) -+ } -+ } -> -> cat("Compiling recursive function...\n") -Compiling recursive function... -> fac <- rcp::rcp_cmpfun(fac, list(name="fac")) - -Breakpoint 1, __jit_debug_register_code () at gdb_jit.c:XXX -53 { -gdb_jit_register (func_name=func_name@entry=0xADDR "fac", code_addr=code_addr@entry=0xADDR , code_size=code_size@entry=4146, inst_addrs=inst_addrs@entry=0xADDR, instruction_count=instruction_count@entry=12, stencils=stencils@entry=0xADDR) at gdb_jit.c:XXX -871 __jit_debug_descriptor.action_flag = JIT_NOACTION; -Breakpoint 2 at 0xADDR: file /tmp/rcp_jit_XXXXXX/fac.S, line 1. -> -> cat("Executing recursive function...\n") -Executing recursive function... -> res <- fac(3) - -Breakpoint 2, fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -1 GETVAR_OP_ -[GDB] Hit fac (1st call). Backtrace:#0 fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -#1 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#2 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#3 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#4 0xADDR in R_execClosure (call=call@entry=0xADDR, newrho=newrho@entry=0xADDR, sysparent=, rho=rho@entry=0xADDR, arglist=arglist@entry=0xADDR, op=op@entry=0xADDR) at eval.c:XXX -#5 0xADDR in applyClosure_core (call=call@entry=0xADDR, op=op@entry=0xADDR, arglist=0xADDR, rho=rho@entry=0xADDR, suppliedvars=, unpromise=unpromise@entry=TRUE) at eval.c:XXX -#6 0xADDR in Rf_applyClosure (call=0xADDR, op=0xADDR, arglist=, rho=0xADDR, suppliedvars=, unpromise=TRUE) at eval.c:XXX -#7 Rf_eval (e=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#8 0xADDR in do_set (call=0xADDR, op=0xADDR, args=0xADDR, rho=0xADDR) at Rinlinedfuns.h:XXX -#9 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#10 0xADDR in Rf_ReplIteration (rho=rho@entry=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0, state=state@entry=0xADDR) at main.c:XXX -#11 0xADDR in R_ReplConsole (rho=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0) at main.c:XXX -#12 0xADDR in run_Rmainloop () at main.c:XXX -#13 0xADDR in Rf_mainloop () at main.c:XXX -#14 0xADDR in main (ac=, av=) at Rmain.c:XXX - -Breakpoint 2, fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -1 GETVAR_OP_ -[GDB] Hit fac (2nd call - recursive). Backtrace:#0 fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -#1 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#2 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#3 0xADDR in Rsh_Call (stack=0xADDR, call=0xADDR, rho=0xADDR) at runtime.h:XXX -#4 0xADDR in fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:10 -#5 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#6 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#7 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#8 0xADDR in R_execClosure (call=call@entry=0xADDR, newrho=newrho@entry=0xADDR, sysparent=, rho=rho@entry=0xADDR, arglist=arglist@entry=0xADDR, op=op@entry=0xADDR) at eval.c:XXX -#9 0xADDR in applyClosure_core (call=call@entry=0xADDR, op=op@entry=0xADDR, arglist=0xADDR, rho=rho@entry=0xADDR, suppliedvars=, unpromise=unpromise@entry=TRUE) at eval.c:XXX -#10 0xADDR in Rf_applyClosure (call=0xADDR, op=0xADDR, arglist=, rho=0xADDR, suppliedvars=, unpromise=TRUE) at eval.c:XXX -#11 Rf_eval (e=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#12 0xADDR in do_set (call=0xADDR, op=0xADDR, args=0xADDR, rho=0xADDR) at Rinlinedfuns.h:XXX -#13 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#14 0xADDR in Rf_ReplIteration (rho=rho@entry=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0, state=state@entry=0xADDR) at main.c:XXX -#15 0xADDR in R_ReplConsole (rho=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0) at main.c:XXX -#16 0xADDR in run_Rmainloop () at main.c:XXX -#17 0xADDR in Rf_mainloop () at main.c:XXX -#18 0xADDR in main (ac=, av=) at Rmain.c:XXX - -Breakpoint 2, fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -1 GETVAR_OP_ -[GDB] Hit fac (3rd call - recursive). Backtrace:#0 fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:1 -#1 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#2 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#3 0xADDR in Rsh_Call (stack=0xADDR, call=0xADDR, rho=0xADDR) at runtime.h:XXX -#4 0xADDR in fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:10 -#5 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#6 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#7 0xADDR in Rsh_Call (stack=0xADDR, call=0xADDR, rho=0xADDR) at runtime.h:XXX -#8 0xADDR in fac (stack=0xADDR, stack@entry=, locals=0xADDR, locals@entry=) at /tmp/rcp_jit_XXXXXX/fac.S:10 -#9 0xADDR in rcpNativeCaller (stack=stack@entry=0xADDR, locals=, call=) at eval.c:XXX -#10 0xADDR in rcpEval (body=body@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#11 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#12 0xADDR in R_execClosure (call=call@entry=0xADDR, newrho=newrho@entry=0xADDR, sysparent=, rho=rho@entry=0xADDR, arglist=arglist@entry=0xADDR, op=op@entry=0xADDR) at eval.c:XXX -#13 0xADDR in applyClosure_core (call=call@entry=0xADDR, op=op@entry=0xADDR, arglist=0xADDR, rho=rho@entry=0xADDR, suppliedvars=, unpromise=unpromise@entry=TRUE) at eval.c:XXX -#14 0xADDR in Rf_applyClosure (call=0xADDR, op=0xADDR, arglist=, rho=0xADDR, suppliedvars=, unpromise=TRUE) at eval.c:XXX -#15 Rf_eval (e=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#16 0xADDR in do_set (call=0xADDR, op=0xADDR, args=0xADDR, rho=0xADDR) at Rinlinedfuns.h:XXX -#17 0xADDR in Rf_eval (e=e@entry=0xADDR, rho=rho@entry=0xADDR) at eval.c:XXX -#18 0xADDR in Rf_ReplIteration (rho=rho@entry=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0, state=state@entry=0xADDR) at main.c:XXX -#19 0xADDR in R_ReplConsole (rho=0xADDR, savestack=savestack@entry=0, browselevel=browselevel@entry=0) at main.c:XXX -#20 0xADDR in run_Rmainloop () at main.c:XXX -#21 0xADDR in Rf_mainloop () at main.c:XXX -#22 0xADDR in main (ac=, av=) at Rmain.c:XXX -> stopifnot(res == 6) -> cat("Result:", res, "\n") -Result: 6 -> - -Breakpoint 1, __jit_debug_register_code () at gdb_jit.c:XXX -53 { -Function "__jit_debug_register_code" not defined. diff --git a/rcp/tests/gdb-jit/gdb-recursion/test.gdb b/rcp/tests/gdb-jit/gdb-recursion/test.gdb index 0e533b9..83175f2 100644 --- a/rcp/tests/gdb-jit/gdb-recursion/test.gdb +++ b/rcp/tests/gdb-jit/gdb-recursion/test.gdb @@ -14,18 +14,21 @@ break fac continue # Hit fac(3) -echo [GDB] Hit fac (1st call). Backtrace: +echo ===BT1_START===\n bt +echo ===BT1_END===\n continue # Hit fac(2) -echo [GDB] Hit fac (2nd call - recursive). Backtrace: +echo ===BT2_START===\n bt +echo ===BT2_END===\n continue # Hit fac(1) -echo [GDB] Hit fac (3rd call - recursive). Backtrace: +echo ===BT3_START===\n bt +echo ===BT3_END===\n continue quit diff --git a/rcp/tests/gdb-jit/run-gdb-tests.py b/rcp/tests/gdb-jit/run-gdb-tests.py old mode 100755 new mode 100644 index 2f44a1a..a8c1f0e --- a/rcp/tests/gdb-jit/run-gdb-tests.py +++ b/rcp/tests/gdb-jit/run-gdb-tests.py @@ -1,331 +1,83 @@ #!/usr/bin/env python3 -""" -GDB debugging test runner for RCP JIT. +"""GDB JIT test runner — validates structural properties of backtraces.""" -Combines test discovery, execution, and output validation into a single script. -Runs GDB tests in specified directories and reports results with colored output. +import os, re, subprocess, sys -Usage: - ./run-gdb-tests.py [--update] TEST_DIRS... - -Examples: - ./run-gdb-tests.py gdb-basic gdb-next # Run specific tests - ./run-gdb-tests.py --update gdb-basic # Update expected output -""" - -import os -import re -import shlex -import subprocess -import sys -import difflib -from pathlib import Path - -try: - from rich.console import Console - from rich.syntax import Syntax - from rich import print as rprint - RICH_AVAILABLE = True -except ImportError: - RICH_AVAILABLE = False - def rprint(*args, **kwargs): - # Strip rich markup for fallback - import re - text = " ".join(str(a) for a in args) - text = re.sub(r'\[/?[^\]]+\]', '', text) - print(text, **kwargs) - -# Patterns to ignore in output normalization -IGNORE_LINES = [ - r'^\[Thread debugging using libthread_db enabled\]$', - r'^Using host libthread_db library .*', - r"^warning: could not find '\.gnu_debugaltlink' .*", - r'^\[Detaching after vfork from child process PID\]$', - r'^warning: Error disabling address space randomization: Operation not permitted$' -] -IGNORE_PATTERNS = [re.compile(p) for p in IGNORE_LINES] - -# Normalization patterns -HEX_PATTERN = re.compile(r'0x[0-9a-fA-F]+') -JIT_PATH_PATTERN = re.compile(r'/tmp/rcp_jit_[a-zA-Z0-9]+/') -PROCESS_PATTERN = re.compile(r'process \d+') -THREAD_PATTERN = re.compile(r'Thread \d+') -C_LINE_PATTERN = re.compile(r' at (?:.*[/\\])?([^/\\]+\.[ch]):\d+') - -# Patterns indicating broken backtraces (should never appear in expected output) -BACKTRACE_BAD_PATTERNS = [ - re.compile(r'#\d+\s+0xADDR in \?\? \(\)'), - re.compile(r'Backtrace stopped:.*corrupt stack'), -] - -# Test timeout in seconds +R_HOME = os.environ["R_HOME"] +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TIMEOUT = 60 - -def normalize_output(content: str) -> str: - """Normalize GDB output to allow comparison across runs.""" - lines = content.splitlines() - normalized_lines = [] - - for line in lines: - line = line.strip() - - # Normalize variable parts - line = HEX_PATTERN.sub('0xADDR', line) - line = JIT_PATH_PATTERN.sub('/tmp/rcp_jit_XXXXXX/', line) - line = PROCESS_PATTERN.sub('process PID', line) - line = THREAD_PATTERN.sub('Thread PID', line) - line = C_LINE_PATTERN.sub(r' at \1:XXX', line) - - # Skip ignored lines - if any(p.match(line) for p in IGNORE_PATTERNS): +def check_gdb_jit(): + r = subprocess.run( + [f"{R_HOME}/bin/Rscript", "-e", + "library(rcp); if(!.Call('rcp_gdb_jit_support', PACKAGE='rcp')) quit(status=1)"], + capture_output=True, timeout=30, env={**os.environ, "RCP_GDB_JIT": "1"}) + return r.returncode == 0 + +def run_gdb(test_dir): + r_bin = f"{R_HOME}/bin/exec/R" + return subprocess.run( + ["gdb", "-q", "-batch", "-x", "test.gdb", "--args", r_bin, "-q", "-f", "test.R"], + capture_output=True, timeout=TIMEOUT, cwd=test_dir, + env={**os.environ, "LD_LIBRARY_PATH": f"{R_HOME}/lib", + "R_HOME": R_HOME, "RCP_GDB_JIT": "1"} + ).stdout.decode("utf-8", errors="replace") + +def extract_bt(output, tag): + m = re.search(f"==={tag}_START===\\n(.*?)==={tag}_END===", output, re.DOTALL) + return m.group(1) if m else None + +def bt_frames(bt): + return [l for l in bt.splitlines() if re.match(r"#\d+", l)] + +def check(cond, msg, errors): + if not cond: + errors.append(msg) + +def test_gdb_recursion(output, errors): + check("Result: 6" in output, "missing 'Result: 6'", errors) + check("corrupt stack" not in output.lower(), "corrupt stack detected", errors) + for i, tag in enumerate(["BT1", "BT2", "BT3"], 1): + bt = extract_bt(output, tag) + check(bt is not None, f"{tag} section not found", errors) + if not bt: continue - - normalized_lines.append(line) - - return "\n".join(normalized_lines) + "\n" - - -def check_gdb_jit_support(r_home: str) -> bool: - """Check if GDB JIT support is enabled in rcp.""" - rscript = os.path.join(r_home, "bin", "Rscript") - cmd = [rscript, "-e", - "library(rcp); if(!.Call('rcp_gdb_jit_support', PACKAGE='rcp')) quit(status=1)"] - - env = os.environ.copy() - env["RCP_GDB_JIT"] = "1" - - try: - result = subprocess.run(cmd, capture_output=True, timeout=30, env=env) - return result.returncode == 0 - except (subprocess.TimeoutExpired, FileNotFoundError): - return False - - -def run_single_test(test_dir: Path, r_home: str, update_mode: bool = False) -> tuple[bool, str]: - """ - Run a single GDB test in the specified directory. - - Returns: - (success, message) tuple - """ - test_gdb = test_dir / "test.gdb" - test_r = test_dir / "test.R" - expected_file = test_dir / "expected.log" - actual_file = test_dir / "actual.log" - - # Validate test directory - if not test_gdb.exists(): - return False, f"Missing test.gdb in {test_dir}" - if not test_r.exists(): - return False, f"Missing test.R in {test_dir}" - - r_bin = os.path.join(r_home, "bin", "exec", "R") - r_lib = os.path.join(r_home, "lib") - - if not os.path.exists(r_bin): - return False, f"R binary not found at {r_bin}" - - # Build GDB command - gdb_cmd = [ - "gdb", "-q", "-batch", - "-x", str(test_gdb), - "--args", r_bin, "-q", "-f", str(test_r) - ] - - env = os.environ.copy() - env["LD_LIBRARY_PATH"] = r_lib - env["R_HOME"] = r_home - env["RCP_GDB_JIT"] = "1" - - # Run GDB - env_prefix = f"LD_LIBRARY_PATH={shlex.quote(r_lib)} R_HOME={shlex.quote(r_home)} RCP_GDB_JIT=1" - print(f"$ {env_prefix} {shlex.join(gdb_cmd)}") - try: - result = subprocess.run( - gdb_cmd, - capture_output=True, - timeout=TIMEOUT, - env=env, - cwd=test_dir - ) - output = result.stdout.decode('utf-8', errors='replace') - output += result.stderr.decode('utf-8', errors='replace') - except subprocess.TimeoutExpired: - return False, "GDB command timed out" - except Exception as e: - return False, f"GDB command failed: {e}" - - # Write actual output - with open(actual_file, 'w', encoding='utf-8') as f: - f.write(output) - - # Normalize output - normalized_actual = normalize_output(output) - - # Backtrace quality check - bad_lines = [] - for line in normalized_actual.splitlines(): - for pat in BACKTRACE_BAD_PATTERNS: - if pat.search(line): - bad_lines.append(line.strip()) - break - - # Update mode: write expected output and return - if update_mode: - if bad_lines: - msg = "Updated expected output, but WARNING: broken backtrace detected:\n" - for bl in bad_lines: - msg += f" {bl}\n" - with open(expected_file, 'w', encoding='utf-8') as f: - f.write(normalized_actual) - return False, msg - with open(expected_file, 'w', encoding='utf-8') as f: - f.write(normalized_actual) - return True, "Updated expected output" - - # Compare with expected output - if not expected_file.exists(): - return False, f"Missing {expected_file}. Use --update to create it." - - with open(expected_file, 'r', encoding='utf-8', errors='replace') as f: - expected_content = f.read() - - # Sanity check: warn if expected.log itself contains broken backtraces - for line in expected_content.splitlines(): - for pat in BACKTRACE_BAD_PATTERNS: - if pat.search(line): - return False, ( - f"expected.log contains broken backtrace pattern:\n" - f" {line.strip()}\n" - f"Re-record with --update after fixing the backtrace issue." - ) - - if normalized_actual == expected_content: - if bad_lines: - return False, ( - "Output matches expected, but backtrace is broken:\n" + - "\n".join(f" {bl}" for bl in bad_lines) - ) - return True, "Output matches expected" - else: - # Generate diff - diff = difflib.unified_diff( - expected_content.splitlines(), - normalized_actual.splitlines(), - fromfile=f"Expected ({expected_file.name})", - tofile="Actual (normalized)", - lineterm="" - ) - diff_text = "\n".join(diff) - return False, diff_text - - -def print_diff(diff_text: str): - """Print diff with syntax highlighting if available.""" - if RICH_AVAILABLE and sys.stdout.isatty(): - console = Console() - syntax = Syntax(diff_text, "diff", theme="ansi_dark", line_numbers=False) - console.print(syntax) - else: - print(diff_text) - - -def main(): - # Parse arguments manually - args = sys.argv[1:] - update_mode = "--update" in args - if update_mode: - args.remove("--update") - test_dirs_args = args - - # Get R_HOME - r_home = os.environ.get("R_HOME") - if not r_home: - rprint("[bold red]Error:[/bold red] R_HOME environment variable is not set.") - sys.exit(1) - - # Check GDB JIT support - rprint("[bold blue]Checking for GDB JIT support...[/bold blue]") - if not check_gdb_jit_support(r_home): - rprint("[yellow]Skipping debugging tests (GDB JIT support not available)[/yellow]") - sys.exit(0) - rprint("[green]GDB JIT support enabled.[/green]") - - # Determine script directory and test directories - script_dir = Path(__file__).parent.resolve() - - if not test_dirs_args: - rprint("[yellow]No test directories specified. Nothing to run.[/yellow]") - sys.exit(0) - - test_dirs = [Path(d) if os.path.isabs(d) else script_dir / d - for d in test_dirs_args] - - # Validate directories exist - for d in test_dirs: - if not d.exists(): - rprint(f"[bold red]Error:[/bold red] Test directory not found: {d}") - sys.exit(1) - - # Run tests - rprint() - rprint(f"[bold]Running {len(test_dirs)} test(s)...[/bold]") - rprint() - - total = 0 - passed = 0 - failed = 0 - failures = [] - - for test_dir in test_dirs: - total += 1 - test_name = test_dir.name - - rprint(f"[bold cyan]{'─' * 60}[/bold cyan]") - rprint(f"[bold]Test:[/bold] {test_name}") - - success, message = run_single_test( - test_dir, r_home, - update_mode=update_mode - ) - - if success: - passed += 1 - if update_mode: - rprint(f" [bold blue]UPDATED[/bold blue] {message}") - else: - rprint(f" [bold green]PASS[/bold green] {message}") - else: - failed += 1 - failures.append(test_name) - rprint(f" [bold red]FAIL[/bold red]") - if message and not message.startswith("---"): - rprint(f" {message}") - elif message: - # It's a diff - print_diff(message) - - # Summary - rprint() - rprint(f"[bold cyan]{'═' * 60}[/bold cyan]") - rprint(f"[bold]Summary:[/bold]") - rprint(f" Total: {total}") - rprint(f" Passed: [green]{passed}[/green]") - rprint(f" Failed: [red]{failed}[/red]") - - if failures: - rprint() - rprint("[bold red]Failed tests:[/bold red]") - for name in failures: - rprint(f" • {name}") - - rprint(f"[bold cyan]{'═' * 60}[/bold cyan]") - - if failed > 0: - sys.exit(1) + frames = bt_frames(bt) + check(not any("?? ()" in f for f in frames), f"{tag}: unresolved frames", errors) + check(any("main" in f for f in frames[-1:]), f"{tag}: doesn't end with main", errors) + fac_count = sum(1 for f in frames if re.search(r"\bfac\b", f)) + check(fac_count == i, f"{tag}: expected {i} fac frame(s), got {fac_count}", errors) + +def test_gdb_next(output, errors): + check("corrupt stack" not in output.lower(), "corrupt stack detected", errors) + bt = extract_bt(output, "BT1") + check(bt is not None, "BT1 section not found", errors) + if bt: + frames = bt_frames(bt) + check(not any("?? ()" in f for f in frames), "BT1: unresolved frames", errors) + check(any("main" in f for f in frames[-1:]), "BT1: doesn't end with main", errors) + check(any("f_jit" in f for f in frames), "BT1: f_jit not in backtrace", errors) + for val in ["10.000000", "1.000000", "11.000000"]: + check(f"dbl: {val}" in output, f"missing 'dbl: {val}'", errors) + +TESTS = {"gdb-recursion": test_gdb_recursion, "gdb-next": test_gdb_next} + +if not check_gdb_jit(): + print("Skipping debugging tests (GDB JIT support not available)") sys.exit(0) +failed = 0 +for name in sys.argv[1:]: + test_dir = os.path.join(SCRIPT_DIR, name) + output = run_gdb(test_dir) + errors = [] + TESTS[name](output, errors) + if errors: + failed += 1 + print(f" FAIL {name}") + for e in errors: + print(f" - {e}") + else: + print(f" PASS {name}") -if __name__ == "__main__": - main() +sys.exit(1 if failed else 0) diff --git a/rcp/tests/list-tests.sh b/rcp/tests/list-tests.sh deleted file mode 100644 index 3a54bb6..0000000 --- a/rcp/tests/list-tests.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# Discover all test subdirectories -# Output: space-separated list of test directory names -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -ls -d "$SCRIPT_DIR"/*/ 2>/dev/null | xargs -n1 basename | sort | tr '\n' ' ' | sed 's/ $//' diff --git a/rcp/tests/perf/Makefile b/rcp/tests/perf/Makefile deleted file mode 100644 index e69de29..0000000 diff --git a/rcp/tests/smoketest/Makefile b/rcp/tests/smoketest/Makefile index 3a85f48..d12f95d 100644 --- a/rcp/tests/smoketest/Makefile +++ b/rcp/tests/smoketest/Makefile @@ -1,6 +1,6 @@ include ../../common.mk -TESTS = basic.R pkgcmp.R cmppkg-dot-functions.R dotcall-issue-12.R ggplot2-unwinding.R cmppkg-s3-generics.R +TESTS = $(wildcard *.R) .PHONY: all test clean diff --git a/rcp/tests/smoketest/cmppkg-base.R b/rcp/tests/smoketest/cmppkg-base.R new file mode 100644 index 0000000..b2da6ef --- /dev/null +++ b/rcp/tests/smoketest/cmppkg-base.R @@ -0,0 +1,19 @@ +library(rcp) + +# Test: compile base package and verify functions work +stopifnot(!rcp_is_compiled(base::Reduce)) +stopifnot(!rcp_is_compiled(base::Filter)) +stopifnot(!rcp_is_compiled(base::Map)) + +rcp_cmppkg("base") + +stopifnot(rcp_is_compiled(base::Reduce)) +stopifnot(rcp_is_compiled(base::Filter)) +stopifnot(rcp_is_compiled(base::Map)) + +# Verify compiled base functions still work correctly +stopifnot(Reduce("+", 1:5) == 15) +stopifnot(identical(Filter(is.numeric, list(1, "a", 2)), list(1, 2))) +stopifnot(identical(Map("+", 1:3, 4:6), list(5L, 7L, 9L))) + +cat("OK\n") diff --git a/rcp/tests/smoketest/pkgcmp.R b/rcp/tests/smoketest/cmppkg-replacement.R similarity index 100% rename from rcp/tests/smoketest/pkgcmp.R rename to rcp/tests/smoketest/cmppkg-replacement.R diff --git a/rcp/tests/smoketest/dotcall-issue-12.R b/rcp/tests/smoketest/dotcall-issue-12.R index b69c74c..75895f5 100644 --- a/rcp/tests/smoketest/dotcall-issue-12.R +++ b/rcp/tests/smoketest/dotcall-issue-12.R @@ -18,6 +18,13 @@ test_dotcall_str <- rcp::rcp_cmpfun( stopifnot(isTRUE(test_dotcall_str(test_dotcall))) stopifnot(isFALSE(test_dotcall_str(function() 1))) +# Test .Call with string and no PACKAGE (fallback path via do_dotcall) +test_dotcall_no_pkg <- rcp::rcp_cmpfun( + function(x) .Call("rcp_is_compiled", x), + list(name="test_dotcall_no_pkg")) +stopifnot(isTRUE(test_dotcall_no_pkg(test_dotcall))) +stopifnot(isFALSE(test_dotcall_no_pkg(function() 1))) + # Test DOTCALL with 0 args test_dotcall_0 <- rcp::rcp_cmpfun( function() .Call(rcp:::C_rcp_dwarf_support), diff --git a/rcp/tests/smoketest/jit.R b/rcp/tests/smoketest/jit.R new file mode 100644 index 0000000..a18b6fd --- /dev/null +++ b/rcp/tests/smoketest/jit.R @@ -0,0 +1,20 @@ +library(rcp) + +# Enable the RCP JIT (hooks into R's JIT mechanism) +rcp_jit_enable() + +# Test 1: global function gets JIT-compiled when called +my_add <- function(x, y) x + y +stopifnot(!rcp_is_compiled(my_add)) +for (i in 1:10) my_add(1L, 2L) +stopifnot(rcp_is_compiled(my_add)) +stopifnot(my_add(3, 4) == 7) + +rcp_jit_disable() + +# Test 2: after disabling, new functions should not be JIT-compiled +new_fun <- function(x) x * 3 +for (i in 1:10) new_fun(1L) +stopifnot(!rcp_is_compiled(new_fun)) + +cat("OK\n")