--- title: "The Runtime Inside Every Translated Program" author: "Songyue Chen" date: "`r Sys.Date()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{The Runtime Inside Every Translated Program} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` ## What a bundle contains A run of `sas_translate()` has an offline `START_HERE.html`, reports, saved outputs, diagnostics, and an editable `bundle/`: ```text bundle/ README.md run.R autoexec.R programs/ <- selected programs and included modules macros/ <- called macros as R functions runtime/ sas2r-helpers.R _sas2r_formats.R output/ <- manual output only, created when needed ``` All available selected code is included, even if it failed checks or execution. Missing code is identified in the run manifest. The bundle runs without sas2r installed: `autoexec.R` loads its own runtime, formats, and macro definitions. Install the R packages listed in its README and configure input paths there. The runtime is generated from the same `R/runtime-*.R` source as the package. ## Running a program Export the bundle with `sas_write()`, then run `Rscript run.R` from that folder. The examples below copy an existing export into a temporary directory before running it. Set `exported_bundle` to the bundle you want to inspect: ```r exported_bundle <- "run_20260907T120000Z_1a2b3c4d" bundle <- tempfile("sas2r-runtime-example-") dir.create(bundle) file.copy(list.files(exported_bundle, full.names = TRUE, all.files = TRUE, no.. = TRUE), bundle, recursive = TRUE) source(file.path(bundle, "run.R"), local = new.env(), chdir = TRUE) ``` To run selected programs yourself, load the configuration once and run required upstream programs before their consumers: ```r runtime <- new.env() source(file.path(bundle, "autoexec.R"), local = runtime, chdir = TRUE) source(file.path(bundle, "programs", "adsl.R"), local = runtime, chdir = TRUE) ``` Manual dataset writes go to `output/datasets//`, WORK writes to `output/work/`, and the launcher runs relative report writers in `output/tlf/`. These writes do not change saved automated outputs or reports. Set a fresh `.sas2r_output_root` when retaining separate manual iterations or testing supplied upstream data: generated members take precedence over input members. Other relative file access and explicit script paths may need human adjustment. The generated bundle README contains concrete dependency-order commands and instructions for moving the entire bundle, copying or configuring data, and checking edited results. Loading a macro defines a function; it does not run the macro as a separate program. ## Maintaining `autoexec.R` Open it when data moves. The LIBRARIES section defines a list like the one below. `local()` keeps this example separate from the loaded runtime: ```r local({ .sas2r_output_root <- runtime$.sas2r_output_root .sas2r_registry <- list( adam = list(read_path = "/study/data/adam", write_path = file.path(.sas2r_output_root, "datasets", "adam"), engine = "sas7bdat", write = "rds"), work = list(read_path = file.path(.sas2r_output_root, "work"), write_path = file.path(.sas2r_output_root, "work"), engine = "rds", write = "rds") ) }) ``` A relative path means *inside this folder*; an absolute path is used as is. So a run folder can be moved or renamed **as a whole** -- the session `work` library and every library the programs write travel with it -- and only the source data the programs read, recorded absolutely, needs an edit when it moves. A commented `` entry is a library some program reads and nothing binds: set its path and uncomment it. A `LIBNAME` inside a program still binds its libref where it stands, exactly as in SAS, so `autoexec.R` is the seed. Nothing under LOAD needs editing. A single program moved out of its folder stops with a message, because its runtime stayed behind. ## Using the runtime interactively Source `autoexec.R` once per session, with `chdir = TRUE` so its own folder is the working directory while it loads -- the R counterpart of running a SAS autoexec. Load the library paths, helpers and formats into a dedicated environment. Programs sourced into that environment find the runtime already loaded and skip their own bootstrap: ```r runtime <- new.env() source(file.path(bundle, "autoexec.R"), local = runtime, chdir = TRUE) adsl <- runtime$lib_read("adam", "adsl") # honours the registry's paths and formats adsl$USUBJID # column names resolve case-insensitively runtime$sas_sort(adsl, by = "usubjid") ``` The helpers `autoexec.R` loads are the bundle's own copy, which is the same code sas2r exports, so this works with or without sas2r installed; attach the package (`library(sas2r)`) when you want the help pages. Two rules the data-access helpers enforce, because a wrong call in a translated program should fail loudly rather than be reinterpreted: `lib_read()` takes the libref and member as two separate strings, and `lib_write()` takes the data frame first -- `lib_write(df, "adam", "adsl")`. Combined `"lib.member"` strings and single-argument calls are rejected with the canonical form in the message. ## The reference The full reference is the help topic: ```r library(sas2r) ?sas2r_runtime # the overview, grouped ?lib_read # each exported helper has its own page and examples ``` In brief, the runtime covers: | Group | Helpers | |---|---| | Data access | `lib_read()`, `lib_write()`, `sas2r_libname_assign()`, `sas2r_libname_clear()`, `sas2r_fold_names()`, the `sas2r_dataset` `$`/`[[` methods | | SAS value semantics | `chr_cmp()`, `%notin%`, `%+%`, `sas_if_else()`, `sas_sum()`, `sas_mean()`, `sas_min()`, `sas_max()`, `sas_round()`, `sas_length()`, `sas_substr()`, `sas_compress()`, `sas_display()` | | Sorting and merging | `sas_sort()`, `sas_merge()` | | Formats | `apply_format()`, `sas_put()` | | Includes | `sas2r_source_include()` | | Internal (generated code only, not exported) | `sas2r_registry_env()`, `sas2r_lib_entry()`, `sas2r_lib_member_path()`, `sas2r_libref_stop()`, `split_ds()` | Every one of them reproduces a SAS behavior R does not share by default: missing values sort low and compare the SAS way, trailing blanks are cosmetic, rounding is half away from zero, `SUM()` and `MEAN()` are row-wise, `MERGE` follows SAS's overlap and `IN=` rules. Startup loads compiled `PROC FORMAT` definitions into `.sas2r_formats`. Generated programs and macro functions can use `sas_put(x, "status.")` or `apply_format(x, "$status.")` directly. Names ignore case and accept a trailing period; the `$` prefix distinguishes character formats. An unknown name raises an error. The selected setup R file is source context, not an additional startup script, so its assignments do not create runtime bindings in smoke or bundle runs. Format widths and decimal specifications (for example, `status8.`) are unsupported and raise an explicit error; removing a width can change the source's output. Conflicting definitions of the same format name are omitted from the catalog and reported by preflight. Execution is deferred because one startup catalog cannot preserve changes to a format during a SAS session. Equivalent repeated definitions remain supported, including reordered values and non-overlapping numeric ranges. Outside a bundle, pass a compiled catalog explicitly: ```r catalog <- list(status = list(values = c("0" = "No", "1" = "Yes"))) sas_put(c(0, 1), "STATUS.", catalog = catalog) ``` For `MERGE ... BY`, values may persist within a BY group. They reset at the next group: if an input has no row for a key, its unique columns are missing for that key. Filling them from a preceding key changes the SAS behavior. ## Versioning and stability The first line of every vendored `sas2r-helpers.R` states which sas2r version generated it: ``` # sas2r runtime helpers -- vendored by sas2r 0.5.8. Do not edit by hand. ``` A bundle always loads its own copy of the runtime, so upgrading sas2r does not replace that copy. Results still depend on inputs, R and dependency versions; recheck the outputs when those change. Fixes to the runtime reach a bundle when it is translated again. The promise behind that: - Existing helper contracts are stable, but correctness fixes may change erroneous results. Exported bundles retain their own runtime until regenerated. - A helper is retired only after a replacement ships and the old name has warned for at least one minor version. - The lint allowlist, the help topic, and the runtime file are held in sync by the package's tests, so a helper cannot be added or removed in one place only. When finished with this example, remove its temporary copy: ```r unlink(bundle, recursive = TRUE) ``` ## Where this is going The runtime is now an exported, versioned source of truth inside sas2r, vendored into bundles from that source. When usage justifies it, it becomes a separate `sas2r.runtime` package that organizations can validate once, with an opt-in mode that writes `library(sas2r.runtime)` into bundles.