| Title: | Translate 'SAS' Programs to R with Dependency-Aware Migration |
| Version: | 0.5.8 |
| Description: | Translates 'SAS' programs and projects into R code, orders connected programs by their dependencies, and executes generated code. Compares generated datasets with supplied reference outputs and reports unresolved translation and validation findings. Rule-based translation works offline without a 'SAS' installation. Optional language model translation, review, and repair use a user-configured provider through 'ellmer' and may require an account and API credentials. |
| License: | Apache License (≥ 2) |
| URL: | https://github.com/songyue-chen/sas2r |
| BugReports: | https://github.com/songyue-chen/sas2r/issues |
| Encoding: | UTF-8 |
| RoxygenNote: | 7.3.3 |
| VignetteBuilder: | knitr |
| Depends: | R (≥ 4.1) |
| Imports: | callr, cli (≥ 3.4), clue, filelock, haven, jsonlite, stats, tibble, utils, vctrs, yaml |
| Suggests: | diffdf, dplyr (≥ 1.1.0), ellmer (≥ 0.4.2), ggplot2, knitr, pdftools, pkgload, ps, rmarkdown, S7, testthat (≥ 3.1.7), withr |
| Config/testthat/edition: | 3 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-21 16:33:12 UTC; chuckchen |
| Author: | Songyue Chen [aut, cre] |
| Maintainer: | Songyue Chen <songyue.chen.dev@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-11-03 15:50:02 UTC |
sas2r: Translate 'SAS' Programs to R with Dependency-Aware Migration
Description
Translates 'SAS' programs and projects into R code, orders connected programs by their dependencies, and executes generated code. Compares generated datasets with supplied reference outputs and reports unresolved translation and validation findings. Rule-based translation works offline without a 'SAS' installation. Optional language model translation, review, and repair use a user-configured provider through 'ellmer' and may require an account and API credentials.
Author(s)
Songyue Chen
See Also
Useful links:
Analyze dataset row order independently of content equivalence
Description
Analyze dataset row order independently of content equivalence
Usage
analyze_output_order(reference, candidate, pairs, context = NULL)
Arguments
reference |
Reference data frame or tibble. |
candidate |
Candidate data frame or tibble. |
pairs |
Matched pairs tibble (reference_row, candidate_row). |
context |
Optional alignment context list. |
Value
A list containing order analysis metadata.
Examples
ref <- data.frame(id = 1:3, v = c("a", "b", "c"))
cand <- data.frame(id = c(3, 1, 2), v = c("c", "a", "b"))
pairs <- tibble::tibble(reference_row = 1:3, candidate_row = c(2L, 3L, 1L))
# Content is equivalent; only the row order differs.
res <- analyze_output_order(ref, cand, pairs,
context = list(order_contract = list(vars = "id", descending = FALSE)))
res$order_equivalent
res$reason
Apply a SAS format
Description
apply_format() applies a compiled format catalog entry (as written to a
bundle's _sas2r_formats.R): exact values first, then ranges (each a
list with lo, hi, label), then other for anything unmatched.
Missing input stays missing unless the format defines other. sas_put()
is PUT(x, fmt.) and does the same.
A format name is resolved in the bundle's .sas2r_formats catalog.
Names are case-insensitive, with an optional trailing period. Character
names retain their $ prefix; numeric and character formats are distinct.
Width and decimal specifications are unsupported and raise an error rather
than silently changing the requested formatting.
An unknown name is an error. Startup loads the catalog before programs and
macro functions run; individual formats are not separate R variables.
Usage
apply_format(
x,
fmt,
catalog = get0(".sas2r_formats", envir = parent.frame(), inherits = TRUE)
)
sas_put(
x,
fmt,
catalog = get0(".sas2r_formats", envir = parent.frame(), inherits = TRUE)
)
Arguments
x |
The vector to format. |
fmt |
A format name, or a compiled format: a list with any of |
catalog |
Named list of compiled formats, used only when |
Value
A character vector.
See Also
Other runtime helpers:
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sex <- list(values = c(M = "Male", F = "Female"), other = "Unknown")
apply_format(c("M", "F", "X", NA), sex)
age <- list(ranges = list(list(lo = 0, hi = 17, label = "<18"),
list(lo = 18, hi = 200, label = "18+")))
sas_put(c(5, 40, NA), age)
sas_put(c("M", "F"), "$SEX.", catalog = list("$sex" = sex))
Serialize a diff digest to JSON
Description
Serialize a diff digest to JSON
Usage
as_digest_json(digest)
Arguments
digest |
An object of class |
Value
A JSON string representing the diff digest.
Examples
sas <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
r <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.5))
digest <- diff_digest(compare_datasets(sas, r, keys = "usubjid"))
# The redacted JSON payload that may cross to an LLM.
json <- as_digest_json(digest)
substr(json, 1, 60)
Compare values the way SAS does
Description
SAS comparison of possibly-missing values. Character operands ignore
trailing blanks, and a missing character value ("" or NA) sorts below
every non-missing one; numeric missing sorts below every number.
Usage
chr_cmp(a, b, op = NULL)
Arguments
a, b |
Vectors to compare; recycled to a common length. |
op |
|
Details
Supply an explicit operator in Boolean conditions. Omitting op returns
an ordering result: equality is 0, which R treats as false. To test
equality, use chr_cmp(a, b, op = "==").
Value
With op NULL, an integer vector of -1, 0, 1 (missing
equals missing). With an operator, the logical SAS would produce – never
NA.
See Also
Other runtime helpers:
apply_format(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
chr_cmp(c("A", NA, "B "), c("A", NA, "B"))
chr_cmp(c("A", NA, "B "), c("A", NA, "B"), op = "==")
chr_cmp(c(61, 70, NA), c(61, 60, NA), op = "==")
chr_cmp(c(1, NA, 3), c(1, 2, 2), op = "<")
Compare aligned outputs under SAS-aware tolerance rules
Description
Compare aligned outputs under SAS-aware tolerance rules
Usage
compare_aligned_outputs(
reference,
candidate,
target,
context = NULL,
profile = compare_profile(),
hashes = list(),
resource_state = "complete",
max_examples = OUTPUT_EVIDENCE_MAX_EXAMPLES
)
Arguments
reference |
Reference data frame or tibble. |
candidate |
Candidate data frame or tibble. |
target |
Discovered target metadata row or list. |
context |
Optional alignment context list. |
profile |
Comparison profile. |
hashes |
Hash list. |
resource_state |
Resource status string. |
max_examples |
Maximum number of examples to include. |
Value
An object of class sas2r_comparison_report.
Examples
ref <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
cand <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.5))
target <- list(target_id = "adam.adsl", logical_dataset = "adam.adsl",
role = "output", contributing_unit_ids = integer())
report <- compare_aligned_outputs(ref, cand, target = target)
report$report_id
report$examples
Compare two datasets under SAS-aware tolerance rules
Description
A thin wrapper over the shared cell comparator diff_aligned_cells(): rows
pair by the given keys (or in row order when keys is NULL), and every
cell verdict comes from the same loop the output-review engine uses.
Usage
compare_datasets(base, comp, profile = compare_profile(), keys = profile$keys)
Arguments
base |
The reference dataset (e.g. read from a SAS golden output). |
comp |
The candidate dataset (e.g. produced by translated R). |
profile |
|
keys |
Key columns for row matching; NULL compares in row order. |
Value
An object of class sas2r_comparison.
Examples
sas <- data.frame(usubjid = c("01-001", "01-002", "01-003"),
aval = c(1.0, 2.0, 3.0))
r <- data.frame(usubjid = c("01-001", "01-002", "01-003"),
aval = c(1.0, 2.0, 3.0 + 1e-10))
# A floating-point difference inside tolerance is not a mismatch.
cmp <- compare_datasets(sas, r, keys = "usubjid")
passed(cmp)
# A genuine difference is reported.
r$aval[3] <- 3.5
passed(compare_datasets(sas, r, keys = "usubjid"))
Build a comparison tolerance profile
Description
Every equivalence judgement the comparator makes is parameterized here and nowhere else. Defaults implement the accepted SAS-to-R tolerance rules: SAS null == NA, special missings reported, trailing padding cosmetic, absolute numeric tolerance of 1e-8 and no relative tolerance. Equality uses abs(x-y) <= abs + rel * max(abs(x), abs(y)); relative tolerance can be enabled explicitly for measured quantities.
Usage
compare_profile(
abs = 1e-08,
rel = 0,
sas_null_equals_na = TRUE,
na_tags = c("report", "ignore", "strict"),
padding = c("cosmetic", "strict"),
attrs_cosmetic = c("label", "format.sas"),
overrides = list(),
keys = NULL
)
Arguments
abs |
Absolute numeric tolerance (scalar >= 0). |
rel |
Relative numeric tolerance (scalar >= 0). |
sas_null_equals_na |
Whether SAS missing character values (empty string |
na_tags |
Tagged-NA handling: |
padding |
Trailing blank padding handling: |
attrs_cosmetic |
Character vector of attribute names whose differences are cosmetic. |
overrides |
Named list of per-variable numeric tolerance overrides. |
keys |
Character vector of key column names, or |
Value
An object of class sas2r_profile.
Examples
# Defaults implement the accepted SAS-to-R tolerance rules.
p <- compare_profile()
p$numeric
# Tighten numeric tolerance and make trailing blanks significant.
strict <- compare_profile(abs = 1e-12, rel = 1e-12, padding = "strict")
# Per-variable tolerance override, plus keys used for row matching.
compare_profile(overrides = list(aval = list(abs = 1e-6)),
keys = c("usubjid", "paramcd"))
Build the redacted diff digest (the only comparison artifact that may cross to an LLM). Names, counts, magnitudes, and enumerated hints only – never cell values, never key values.
Description
Build the redacted diff digest (the only comparison artifact that may cross to an LLM). Names, counts, magnitudes, and enumerated hints only – never cell values, never key values.
Usage
diff_digest(x, label = "dataset")
Arguments
x |
An object of class |
label |
Label for the dataset comparison. |
Value
An object of class sas2r_diff_digest.
Examples
sas <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
r <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.5))
cmp <- compare_datasets(sas, r, keys = "usubjid")
# Names, counts and magnitudes only -- never cell values, never key values.
d <- diff_digest(cmp, label = "adsl")
d$vars
d$pattern_hints
Delete explicitly named datasets from a writable library
Description
Removes the stored dataset, rather than just an R object. Missing members produce a message and do not stop deletion of other members. Names are matched case-insensitively across rds, xpt and sas7bdat files.
Usage
lib_delete(libref, members)
Arguments
libref |
A configured library name, such as |
members |
Character vector of explicit dataset names, without librefs or file extensions. An empty vector does nothing. |
Details
Only an explicit vector of ordinary SAS member names is supported. Expand
a known list into names before calling; prefix lists, ranges and _ALL_
are not interpreted. Dynamic selection that cannot be resolved must remain
an explicit unsupported operation, never a silent no-op.
This helper deletes from the configured write directory. If a requested
member also exists in a different read directory, deletion is unsupported:
removing the written copy would expose that original again to lib_read().
The whole request is checked before any file is removed, and input library
files in a separate read directory are never deleted.
Value
The names of removed members, invisibly.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
local({
folder <- tempfile("work")
.sas2r_registry <- list(work = list(path = folder, write = "rds"))
lib_write(data.frame(x = 1), "work", "scratch")
lib_delete("work", "scratch")
unlink(folder, recursive = TRUE)
})
Test whether a library dataset member exists
Description
Uses the same registry and write/read search order as lib_read(), without
reading or parsing data. A present unreadable or zero-row dataset exists.
Invalid arguments and an unknown library remain errors, not FALSE.
Supports ordinary RDS, SAS7BDAT and XPT member files, not SAS views or
arbitrary external paths. Existence does not establish readability.
Usage
lib_exists(libref, member)
Arguments
libref |
A single library name, without a member or path. |
member |
A single dataset member name, without a path or extension. |
Value
A single logical value.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
local({
.sas2r_registry <- list(work = list(path = tempdir()))
lib_exists("work", "not_created")
})
List ordinary dataset members in a configured library
Description
Lists names without reading dataset rows. Uses the same member resolver and
write/read precedence as lib_exists() and lib_read(). Duplicate storage
formats yield one name. Names and extensions must be resolvable on the local
filesystem; case is preserved. Unsupported views, name literals and ranges
are not expanded. An unknown library or inaccessible configured directory is
an error, distinct from an empty library. A not-yet-created write directory
is empty; a missing separate read directory remains a configuration error.
Usage
lib_members(libref)
Arguments
libref |
A single registered library name, without a member or path. |
Details
Listing does not authorize deletion of protected inputs: lib_delete()
retains its separate-input restriction. This is a runtime helper, not an
agent tool or permission to send library contents to a model.
Value
A sorted character vector of ordinary resolvable member names.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
local({
folder <- tempfile()
dir.create(folder)
.sas2r_registry <- list(work = list(path = folder))
lib_write(data.frame(id = 1), "work", "scratch")
members <- lib_members("work")
lib_delete("work", members)
unlink(folder, recursive = TRUE)
})
Read and write datasets through librefs
Description
lib_read("lib", "member") reads a dataset from a libref as the registry
configures it: the libref's write path is tried first (so a dataset the
bundle produced earlier wins), then its read path, for .rds,
.sas7bdat, and .xpt in that order. The result carries class
sas2r_dataset, whose $ and [[ methods resolve column names
case-insensitively, as SAS variable names do. lib_write(df, "lib", "member") writes to the libref's write path in its configured format
(rds or xpt), creating the directory on demand.
Usage
lib_read(libref, member, ...)
lib_write(df, libref, member, ...)
Arguments
libref |
The libref, as a single string. |
member |
The dataset member name, as a single string. |
... |
Not used; present so a rejected alias fails with a clear message. |
df |
The data frame to write. |
Details
Exactly one call form is accepted for each. Combined "lib.member"
strings, single-argument calls, and dataset=/table= aliases fail with
the canonical form in the message, so a wrong call in an agent translation
becomes repair feedback instead of being silently reinterpreted.
The registry is the .sas2r_registry list a bundle's autoexec.R
defines; in a translated program it is loaded by the bootstrap header. To
use these helpers interactively, create runtime <- new.env() and source
that file with local = runtime, chdir = TRUE, then call
runtime$lib_read() or runtime$lib_write().
Value
lib_read() returns the dataset with class sas2r_dataset;
lib_write() returns df, invisibly.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
local({
lib <- tempfile("lib"); dir.create(lib)
.sas2r_registry <- list(
raw = list(read_path = lib, write_path = lib, engine = "rds", write = "rds")
)
lib_write(data.frame(USUBJID = c("01", "02"), AVAL = c(1, 2)), "raw", "dm")
dm <- lib_read("raw", "dm")
dm$usubjid # case-insensitive column access
unlink(lib, recursive = TRUE)
})
Did a comparison pass within tolerance?
Description
Did a comparison pass within tolerance?
Usage
passed(x)
Arguments
x |
An object of class |
Value
Logical indicating whether the comparison passed.
Examples
sas <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
r <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
passed(compare_datasets(sas, r, keys = "usubjid"))
Print a sas2r translation summary
Description
Print a sas2r translation summary
Usage
## S3 method for class 'sas2r_translation'
print(x, ...)
Arguments
x |
A |
... |
Additional arguments (ignored). |
Value
The input x, invisibly.
Print a sas2r transpilation summary
Description
Print a sas2r transpilation summary
Usage
## S3 method for class 'sas2r_transpilation'
print(x, ...)
Arguments
x |
A |
... |
Additional arguments (ignored). |
Value
The input x, invisibly.
Define reusable dataset quality requirements
Description
Assign a profile a name in outputs$profiles, then select it with
outputs$assertions[["adam.adsl"]]$profile. Target assertions override
whole profile fields. These are explicit study requirements, not an
automatic CDISC compliance assessment or evidence of SAS equivalence.
Usage
qc_profile(
required_columns = NULL,
labels = NULL,
formats = NULL,
types = NULL,
column_order = NULL,
keys = NULL,
unique_keys = NULL,
row_count = NULL,
min_rows = NULL,
max_rows = NULL,
numeric_tolerance = NULL,
tolerances = NULL
)
Arguments
required_columns |
Character vector of required column names. |
labels, formats |
Named character vectors of exact required variable
labels and |
types |
Named character vector: |
column_order |
Exact ordered vector of all expected column names. |
keys |
Row alignment columns, matched case-insensitively. Requiring uniqueness also requires nonmissing, nonblank key values. |
unique_keys |
Whether the combined key must be nonmissing and unique. |
row_count, min_rows, max_rows |
Exact, minimum, or maximum row count. |
numeric_tolerance |
Absolute tolerance for reference comparison. When set, the relative tolerance defaults to zero, as for target assertions. |
tolerances |
Named variable overrides with |
Value
A plain list containing only explicitly supplied, non-NULL fields.
Omitted fields inherit global settings. Explicit FALSE and empty lists
override them. Types describe physical R columns: factors do not satisfy
character, even when their values compare equal to character data.
Examples
adsl_qc <- qc_profile(
required_columns = c("USUBJID", "AGE"),
keys = "USUBJID", unique_keys = TRUE,
types = c(USUBJID = "character", AGE = "numeric"),
min_rows = 1, tolerances = list(AGE = list(abs = 0, rel = 0))
)
Read a bounded comparison report from the registry
Description
Read a bounded comparison report from the registry
Usage
read_comparison_report(report_id, registry)
Arguments
report_id |
Report identifier string. |
registry |
Registry environment or list. |
Value
An object of class sas2r_comparison_report.
Examples
ref <- data.frame(usubjid = "01-001", aval = 1.0)
cand <- data.frame(usubjid = "01-001", aval = 2.0)
target <- list(target_id = "adam.adsl", logical_dataset = "adam.adsl",
role = "output", contributing_unit_ids = integer())
report <- compare_aligned_outputs(ref, cand, target = target)
# The registry may be a named list or an environment.
registry <- list()
registry[[report$report_id]] <- report
back <- read_comparison_report(report$report_id, registry)
identical(back$report_id, report$report_id)
Resolve a translated LIBNAME path against the execution root
Description
Registry seed paths are resolved separately by sas2r_resolve_registry().
Usage
sas2r_assignment_path(path, env)
Arguments
path |
Character scalar path, absolute or relative. |
env |
Runtime environment containing |
Value
A normalized character scalar path. Relative paths use the execution root, or the working directory when no root is set. The target need not exist.
Fold column names to lower case
Description
SAS resolves variable names case-insensitively; R does not. Deterministic
translations fold every frame to lower-case names at entry so lower-case
source references (aval) find upper-case data columns (AVAL) the way
SAS would.
Usage
sas2r_fold_names(df)
Arguments
df |
A data frame. |
Value
df with lower-case column names.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
names(sas2r_fold_names(data.frame(USUBJID = 1, AVAL = 2)))
Read one library entry from the loaded registry
Description
Read one library entry from the loaded registry
Usage
sas2r_lib_entry(libref)
Arguments
libref |
Character scalar library name, matched case-insensitively. |
Value
The registry entry as a list, normally with read_path, write_path,
engine, and write fields. Signals sas2r_unknown_libref when absent;
does not read a dataset or return a data frame.
Find a dataset member without reading its contents
Description
Find a dataset member without reading its contents
Usage
sas2r_lib_member_file(reg, member)
Arguments
reg |
A resolved library registry entry. |
member |
A dataset member name, without a path or extension. |
Value
A list containing type and path, or NULL for an absent member.
The write directory precedes the read directory; formats are tried in
RDS, SAS7BDAT, XPT order. Directories are not dataset member files.
Construct a member filename inside a library directory
Description
Construct a member filename inside a library directory
Usage
sas2r_lib_member_path(dir, member, ext)
Arguments
dir |
Character scalar library directory. |
member |
Character scalar dataset member name, without path separators. |
ext |
File extension including its dot, for example |
Value
A character scalar filename. Does not read or create the file.
Invalid member names signal sas2r_libref_member_error.
Bind or clear a libref at its point of use
Description
The registry a bundle starts with is only a seed – the libraries the
configuration declares, plus work. Every LIBNAME the SAS program
executes changes the registry where that statement stood, so a libref that
is rebound or cleared part-way through behaves at run time the way it does
in SAS. sas2r_libname_assign() is LIBNAME libref '<path>';
sas2r_libname_clear() is LIBNAME libref CLEAR.
Relative assignment paths use .sas2r_execution_root from autoexec.R,
initially the source project directory. Registry seed paths still use the
bundle folder. Reassigning a known physical library preserves its separate
write directory. Newly assigned libraries write under the execution output
libraries/ folder unless write_path is explicitly supplied.
Usage
sas2r_libname_assign(
libref,
read_path,
write_path = read_path,
engine = "sas7bdat",
write = "rds"
)
sas2r_libname_clear(libref)
Arguments
libref |
The libref (case-insensitive). |
read_path, write_path |
Directories the libref reads from and writes to. |
engine |
Format of members read: |
write |
Format members are written in: |
Value
sas2r_libname_assign() returns the new registry entry, invisibly;
sas2r_libname_clear() returns NULL, invisibly.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
local({
.sas2r_registry <- list()
sas2r_libname_assign("raw", tempdir(), engine = "rds")
names(.sas2r_registry)
sas2r_libname_clear("raw")
names(.sas2r_registry)
})
Signal a runtime library error
Description
Signal a runtime library error
Usage
sas2r_libref_stop(cls, msg, identifiers = character())
Arguments
cls |
Character scalar giving the specific error class. |
msg |
Character scalar error message. |
identifiers |
Technical names for structured diagnostics, checked against code before agent use. |
Value
Does not return normally. Signals an error with classes cls,
sas2r_libref_error, error, and condition, and fields message, call and identifiers.
Find the environment holding the runtime library registry
Description
Looks first along the runtime's enclosing environments, then active local
call frames. Package helpers exclude the global workspace; standalone
helpers use the environment they were explicitly sourced into. Load
autoexec.R into a local environment for interactive use.
Usage
sas2r_registry_env()
Value
An environment containing .sas2r_registry, not the registry list.
Signals sas2r_no_registry if no registry is loaded.
Resolve a bundle's libref registry against its folder
Description
autoexec.R lists each libref's directories the way a person writes them:
an absolute path is used as is, and a relative path means inside the bundle
folder. This turns the relative ones into absolute paths against root,
creates every write directory, and returns the registry ready for
lib_read() and lib_write(). autoexec.R calls it once, after the
helpers are loaded; call it yourself only when building a registry by hand.
Usage
sas2r_resolve_registry(registry, root)
Arguments
registry |
The |
root |
The bundle folder, as an absolute path. |
Details
A <FILL> placeholder left in a path – the commented entries autoexec.R
carries for a library nothing binds – is refused with the libref named,
rather than becoming a directory called <FILL...>.
Value
The registry, with every read_path and write_path absolute.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
root <- tempfile("bundle"); dir.create(root)
reg <- sas2r_resolve_registry(
list(work = list(read_path = "work", write_path = "work",
engine = "rds", write = "rds")),
root)
reg$work$write_path # <root>/work, and the directory now exists
The runtime every translated program carries
Description
Every program sas2r translates calls a small runtime – lib_read(),
lib_write(), sas_sort(), sas_merge(), chr_cmp(), the format
machinery and the rest. Since sas2r 0.2.0 that runtime is exported package
code with a help page per helper, and every bundle still carries its
own copy as sas2r-helpers.R, rendered from the same source files
(R/runtime-*.R) so the two are identical function for function. A
bundle always loads its own runtime copy, so it can run without sas2r
installed. Results still depend on input data, R and dependency versions;
recheck outputs when those change.
The vignette vignette("runtime-helpers", package = "sas2r") explains how
to run programs, how to use the runtime interactively, and the stability
policy behind it.
How the runtime reaches a program
One guarded line at the top of each generated program sources autoexec.R
into the program's own environment, the way a SAS session runs its autoexec
first. Run Rscript run.R from the bundle folder to execute programs in
dependency order. For individual programs, create runtime <- new.env()
and use source("<bundle>/autoexec.R", local = runtime, chdir = TRUE).
Source required upstream programs into that same environment before
source("<bundle>/programs/adsl.R", local = runtime).
autoexec.R is the bundle's
counterpart of a SAS autoexec: generated by the translation, maintained by
a person afterwards.
Its LIBRARIES section says where each libref reads from and writes to (a
relative path means inside the bundle folder); its LOAD section sources
runtime/sas2r-helpers.R (this runtime), resolves those paths with
sas2r_resolve_registry(), and sources runtime/_sas2r_formats.R (the compiled
format catalog).
To use the runtime at the console, load it into runtime as above and call
helpers through that environment, for example runtime$lib_read("adam", "adsl").
Attach sas2r when you want the help pages.
The helpers
- Data access
lib_read(),lib_write(),sas2r_libname_assign(),sas2r_libname_clear(),sas2r_resolve_registry(),sas2r_fold_names(); datasets read throughlib_read()carry classsas2r_dataset, whose$and[[methods resolve column names case-insensitively.- SAS value semantics
chr_cmp(),sas_if_else(),sas_sum()andsas_mean(),sas_min()andsas_max(),sas_round(),sas_length(),sas_substr(),sas_compress(),sas_display(); and two operators the package defines but does not export:a %notin% b(NOT INwith SAS missing semantics – missing values match missing values in the list; character comparison ignores trailing blanks) anda %+% b(||concatenation where a missing operand contributes nothing rather than"NA"; SAS pads fixed-width operands, this does not).- Sorting and merging
- Formats
- Includes
Internal helpers
Generated code calls these; they are not exported, not meant for direct
use, and may change with the generator: sas2r_registry_env() (where the
.sas2r_registry list lives – lexically where the runtime was loaded,
then active local frames; package helpers exclude the global workspace),
sas2r_lib_entry()
(the registry entry for a libref), sas2r_lib_member_path() (the file a
member resolves to, refusing any name that is not a plain member name),
sas2r_assignment_path() (resolve a LIBNAME path against the execution root),
sas2r_libref_stop() (classed libref errors), split_ds() (split a
two-level SAS name after macro substitution).
Versioning and stability
The runtime follows the sas2r version, and every vendored copy states the
version that generated it. Existing helper contracts are stable, but
correctness fixes may change erroneous results. A helper is retired only after a
replacement ships and the old name has warned for at least one minor
version. Tests hold the source files, the vendored template, the help
pages, and the lint allowlist in sync. A separate sas2r.runtime package
is a future option when usage justifies it.
See Also
sas_translate(), sas_write(), sas_code()
Run an included module where the %INCLUDE stood
Description
Each included SAS file is translated once into its own staged module; this
executes that module at one include site, into the calling program's own
environment, exactly where the %INCLUDE stood.
Usage
sas2r_source_include(relative_path, envir = parent.frame())
Arguments
relative_path |
The module's path relative to the staged module root. |
envir |
The environment to run it in; the caller's by default. |
Details
The path is relative to the staged module root, and never to the working
directory. The editable bundle sets this to programs/ in autoexec.R;
flat execution snapshots use the nearest ancestor holding autoexec.R. Absolute
paths and . or .. components are rejected, and the file the joined path
actually resolves to is then required to lie under the root: rejecting the
components alone would still let a symlinked directory inside the bundle
read outside it, so the confinement is checked on the resolved path, where
a hand-edited bundle cannot talk its way out.
Value
The resolved path of the module, invisibly.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
## Not run:
# inside a translated program, where the SAS source had %include 'inc/prep.sas';
sas2r_source_include("inc/prep.R")
## End(Not run)
Retrieve staged code for a translated program from the selected bundle
Description
Recursively inspects the selected bundle directory and resolves by component ID, relative file path, basename, or integer index.
Usage
sas_code(x, file = 1L)
Arguments
x |
A |
file |
Staged file index or name/component ID. |
Value
Character string containing the program code.
Examples
sas_dir <- file.path(tempdir(), "sas2r-code-example")
dir.create(sas_dir, showWarnings = FALSE)
writeLines(c("proc sort data=work.dm out=work.dm_sorted;",
" by usubjid;",
"run;"), file.path(sas_dir, "sort.sas"))
res <- sas_translate(sas_dir, out_dir = tempfile("sas2r-out"), execute = FALSE)
# Resolve by index, or by file name / component id.
cat(sas_code(res, 1L))
SAS character functions
Description
sas_compress() is COMPRESS: every character listed in chars is
removed literally, including ] and -. SAS modifiers are unsupported. sas_substr() is SUBSTR; a NULL
len reads to the end of the string. sas_length() is LENGTH: trailing
blanks do not count, and a missing or blank value has length 1, as in SAS.
Usage
sas_compress(x, chars = " ")
sas_substr(x, pos, len = NULL)
sas_length(x)
Arguments
x |
A character vector (or a vector coerced to character). |
chars |
Characters to remove. |
pos, len |
Start position and length, as in SAS. |
Value
sas_compress() and sas_substr() return character vectors;
sas_length() an integer vector.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sas_compress("a b c")
sas_substr("abcdef", 2, 3)
sas_length(c("abc ", "", NA))
Load project configuration (optional by design)
Description
Looks for _sas2r.yml upward from start; the SAS2R_CONFIG
environment variable overrides discovery. No config file is the
primary supported case: built-in defaults let a bare SAS script be
scanned, assessed, and translated with no setup at all.
Every relative configured path – libraries, macros.search_path,
includes.roots, environment.autoexec, migration.execution_order, and
output/comparison reference paths – is resolved against the
configuration file's own directory, never against the working directory:
those roots reach %include occurrence identity, which must not depend on
where the scan was launched from. One anchoring rule governs these paths, so a
~-prefixed entry is treated as already carrying its own base everywhere
rather than in some keys only, because R expands ~ against the user's home
directory and prefixing a base onto it would corrupt the path.
Usage
sas_config(path = NULL, start = ".")
Arguments
path |
Path to a configuration file. Defaults to |
start |
Directory from which to search upwards for |
Details
A caller-supplied configuration list has no file of its own, so
sas_project() resolves whatever is still relative against the project
root. Both bases are facts about the project's layout, and both travel with
the checkout.
YAML boolean settings use true and false. Tokens such as N,
Y, yes, and no remain strings, preserving clinical metadata keys.
Quote string metadata values that look numeric, such as SAS format "8.".
migration.execution_order optionally lists every executable root program
exactly once, in its intended order within one shared WORK session. Reads
use the latest preceding completed write; a later writer cannot supply an
earlier read. List roots only: called macros, startup and included files
retain their existing roles. The list orders the scanned source scope;
path and recursive in sas_preflight() or sas_translate() still select
that scope. Omitting the order retains inferred dependencies. Changing the
order on a previously scanned project requires rescanning the sources.
Value
A sas2r_config object containing libraries, macro_search_path,
include_roots, migration, optional normalized llm, output_review, outputs, source, and raw.
Examples
# Read the demo project configuration shipped with the package.
cfg <- sas_config(system.file("examples", "migration-demo", "_sas2r.yml",
package = "sas2r"))
class(cfg)
names(cfg$libraries)
# With no explicit file, search from a known project directory.
cfg <- sas_config(start = system.file("examples", "migration-demo", package = "sas2r"))
Display values the way SAS prints them
Description
Numeric missing is ".", character missing is blank, and numbers are never
in scientific notation.
Usage
sas_display(x)
Arguments
x |
A vector. |
Value
A character vector.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sas_display(c(1, NA, 1e6))
sas_display(c("a", NA))
IF/THEN/ELSE assignment with SAS semantics
Description
A missing condition is false in SAS. The result keeps the class (Date,
labelled) of the branch that carries it, which ifelse() would strip.
Usage
sas_if_else(cond, yes, no)
Arguments
cond |
A logical vector. |
yes, no |
Values for true and false conditions; recycled. |
Value
A vector of the common length.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sas_if_else(c(TRUE, NA, FALSE), "yes", "no")
sas_if_else(c(TRUE, FALSE), as.Date("2026-01-01"), NA)
Construct an LLM adapter from a project's llm: configuration
Description
The supported way to obtain the llm argument that sas_translate() accepts.
The adapter is backed by public ellmer APIs for one of the twelve registered provider
ids. Credential values may be held in memory by adapters and redactors.
Keep adapters private; reports redact registered secret values.
Constructing an adapter contacts no network – use sas_llm_probe() to
validate connectivity and model capabilities, and sas_llm_models() to see
the inventory visible to the configured identity.
Usage
sas_llm(config)
Arguments
config |
An |
Value
A sas2r_llm adapter.
See Also
sas_llm_probe(), sas_llm_models()
Examples
# Constructing an adapter contacts no network.
if (requireNamespace("ellmer", quietly = TRUE)) {
llm <- sas_llm(list(provider = "anthropic", model = "claude-sonnet-4-6"))
class(llm)
}
## Not run:
# Choose one of sas2r's registered providers.
llm <- sas_llm(list(provider = "openai", model = "gpt-4.1"))
res <- sas_translate("path/to/sas", llm = llm)
## End(Not run)
List models visible to the configured provider identity
Description
Uses the provider's read-only inventory route when ellmer exposes one. An unavailable or non-authoritative inventory is reported explicitly and is never represented as an empty list of models. Bedrock inference-profile and custom-runtime selectors require probe-based access validation because the foundation-model inventory does not enumerate them safely.
Usage
sas_llm_models(config)
Arguments
config |
An |
Value
A sas2r_llm_model_inventory record.
Examples
## Not run:
# Contacts the provider's read-only inventory route; needs credentials.
sas_llm_models(list(provider = "anthropic", model = "claude-sonnet-4-6"))
# Or straight from a project configuration.
sas_llm_models(sas_config("path/to/_sas2r.yml"))
## End(Not run)
Probe LLM connectivity and configured settings with a structured ping
Description
Validates authentication, endpoint reachability, and structured-output support and forwarding of explicit parameters on the configured model. The ping uses the configured output ceiling, or 2048 tokens when reasoning effort is configured, otherwise 32. It does not perform the negative control used by automatic translation startup to detect ignored reasoning settings, and does not populate that startup cache. The probe may invoke provider-managed authentication, including device login. A missing or expired ambient session raises a classed condition naming the command to run instead.
Usage
sas_llm_probe(
llm,
max_retries = 2L,
log_dir = tempdir(),
on_charge = NULL,
tier = "cheap",
can_attempt = NULL,
usage_budget = NULL,
audit_context = list()
)
Arguments
llm |
A |
max_retries |
Maximum retry attempts for a retryable failure. |
log_dir |
Directory the audit log is written to. Defaults to the R session's temporary directory; supply a directory to retain the log. |
on_charge |
Deprecated compatibility callback. |
tier |
Tier whose model is probed ( |
can_attempt |
Optional predicate consulted before each attempt. |
usage_budget |
Optional shared usage budget. Pass one with a
|
audit_context |
Optional named list merged into the audit context. |
Details
The ping carries no tools, so it does not exercise tool calling. A model that answers the probe may still lack tool support; that is caught at the first tool-using phase, not here.
An attempt that reaches the provider is written to the audit log. A call
refused before transport – by the capability gate, or by an exhausted
budget – returns FALSE without an audit entry. Reaching the durable
usage ledger additionally requires usage_budget to carry a
ledger_path, as it does when a translation run probes through its own
shared budget; a standalone call with no usage_budget gets a transient
budget with none, so it is audited but not ledgered.
Value
TRUE when the model answered the ping, FALSE on a definitive
nonanswer. Access, authentication, and transport failures are raised as
classed conditions with secrets redacted.
See Also
Examples
## Not run:
# Sends a structured ping to validate connectivity and model access.
llm <- sas_llm(list(provider = "anthropic", model = "claude-sonnet-4-6"))
sas_llm_probe(llm)
## End(Not run)
Merge two datasets the way MERGE ... BY does
Description
MERGE a b; BY by; with IN= semantics selected by keep: "both"
(in_a and in_b), "left" (in_a), "right" (in_b), "left_only",
"right_only", "full". Where both datasets carry a non-key column the
later dataset's value wins, columns follow statement order, and rows follow
SAS BY ordering. Many-to-many keys, and duplicate keys with shared non-key
columns, are refused: those cases require SAS observation-by-observation
semantics that this join-based helper does not implement.
Do not pre-create missing columns on one input when the other input supplies
them: that invents an overlap. Preserve genuine overlapping source columns;
their unsupported repeated-key semantics must not be silently dropped.
Values from an input can be retained within a BY group, but reset to missing
at the next BY group. If an input has no row for a key, its unique columns
are missing for that key; never fill them from a preceding key.
Usage
sas_merge(
a,
b,
by,
keep = c("full", "both", "left", "right", "left_only", "right_only"),
...
)
Arguments
a, b |
Data frames, in statement order. |
by |
Character vector of BY variables. |
keep |
Which rows to keep; see Description. |
... |
Ignored. |
Value
The merged data frame.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
a <- data.frame(id = c(1, 2), x = c("a", "b"))
b <- data.frame(id = c(2, 3), y = c("B", "C"))
sas_merge(a, b, by = "id", keep = "full")
sas_merge(a, b, by = "id", keep = "both")
events <- data.frame(id = c(1, 1, 2), event = c("a", "b", "c"))
subjects <- data.frame(id = c(1, 2), base = c(10, 20))
sas_merge(events, subjects, by = "id", keep = "left") # three events remain
Row-wise MIN and MAX with SAS missing semantics
Description
Row-wise across the arguments, ignoring missing values; an observation
whose arguments are all missing yields missing, and a single argument
passes through elementwise – SAS MIN(x) is never a column aggregate.
Usage
sas_min(...)
sas_max(...)
Arguments
... |
Numeric vectors, recycled to a common length. |
Value
A numeric vector of that length.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_missing(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sas_min(c(1, NA, 3), c(2, NA, 1))
sas_max(c(1, NA, 3), c(2, NA, 1))
SAS missing values and Boolean conditions
Description
Character missing is NA or all spaces. Numeric missing is NA (including tagged missing values). A missing or zero numeric condition is false. Character conditions convert to numeric; nonnumeric text is false.
Usage
sas_missing(x)
sas_true(x)
Arguments
x |
A vector. |
Value
A logical vector with no missing values.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_round(),
sas_sort(),
sas_sum()
Examples
sas_missing(c("", " ", NA, "A"))
sas_true(c(NA, 0, 1, -1))
Inspect a migration and optionally diagnose findings
Description
Scans sources and resolves library bindings with the same project scanner
used by sas_translate(). Reads directory listings, not dataset contents.
Does not execute programs, read dataset contents, or create output directories,
scan caches, or usage ledgers. With a configured LLM, actionable findings can
receive one bounded advisory diagnosis. Use diagnose = "off" for offline
inspection without constructing an adapter or making a model call.
Deterministic support findings are advisory: an AI translation may handle
deferred constructs, and runtime checks can still defer supported shapes.
Usage
sas_preflight(
path,
out_dir = NULL,
config = NULL,
outputs = NULL,
budget_usd = Inf,
budget_mode = "stop",
pricing_source = "catalog",
pricing_rates = NULL,
usage_limits = NULL,
recursive = FALSE,
max_parallel_translations = NULL,
diagnose = c("auto", "off"),
llm = NULL
)
Arguments
path |
Path to a SAS file or directory containing SAS files, or a |
out_dir |
Planned migration output root. |
config |
Optional configuration list, YAML path, or |
outputs |
Optional character vector or list specifying output contract overrides. Reference paths supplied here resolve from the calling working directory. Output overrides are retained in the returned project for reuse. |
budget_usd |
Dollar threshold for LLM spend. Defaults to Inf (unlimited). |
budget_mode |
Dollar enforcement mode: "stop", "observe", "soft", or "strict". Defaults to "stop". |
pricing_source |
Cost provenance source ("catalog", "adapter", "organization", "external"). Defaults to "catalog". |
pricing_rates |
Optional organization pricing table. |
usage_limits |
Optional named list of non-dollar request limits:
|
recursive |
Logical; whether to scan subdirectories recursively. Defaults to FALSE. |
max_parallel_translations |
Concurrent component workflow limit used
when planning translation. Overrides |
diagnose |
|
llm |
Optional |
Details
With diagnose = "auto", a configured model may receive bounded SAS
statements, source paths and findings, subject to budget admission; dataset
contents are not sent. Configured adapters use one transport attempt capped
at 120 seconds; custom supplied adapters retain their transport contract.
Advice can suggest source/configuration corrections or an issue draft for a
suspected sas2r bug. It does not edit sources or submit issues.
Configure migration.execution_order to inspect a declared shared-session
sequence; see sas_config(). pipeline$order_source reports "configured"
or "inferred". A permanent input with no preceding known or possible
writer still receives the normal availability check, even if a later
program writes it. Unknown macro effects can leave dataset state deferred.
Value
A sas2r_preflight list with sources, point-of-use libraries,
inputs (available, missing, unresolved, no_producer, deferred, backward_dependency,
created_if_missing, generated, or environment), references, unsupported,
scanner findings, shared readiness warnings, outputs, destinations, budget, next_actions, diagnosis, and actual model_calls.
Diagnosis failures preserve the static result. Inspection errors retain
their original condition with optional advice attached as $diagnosis.
called_macros lists reachable search-path macro definitions and their
standalone R and interface-test paths.
pipeline reconciles every scanned file with the translation schedule and
bundle execution roles, listing intentional exclusions and blocking gaps.
Also includes status, configured_libraries, schedule, explanatory notes,
max_parallel_translations, and the scanned project, reusable by sas_translate() while sources are unchanged.
ready_for_translation means no known missing inputs or unresolved scan
findings; it does not mean execution or reference validation has passed.
Examples
source <- tempfile(fileext = ".sas")
writeLines("data out; set raw.dm; run;", source)
check <- sas_preflight(source, diagnose = "off")
check$inputs
check$model_calls
unlink(source)
Round the way SAS does
Description
SAS ROUND rounds half away from zero, to a rounding unit; R's round()
is banker's rounding.
Usage
sas_round(x, unit = 1)
Arguments
x |
A numeric vector. |
unit |
The rounding unit (SAS's second argument). |
Value
x rounded to the nearest multiple of unit.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_sort(),
sas_sum()
Examples
sas_round(c(0.5, 1.5, -0.5))
sas_round(12.345, 0.01)
Sort a dataset the way PROC SORT does
Description
Stable, with missing values lowest under SAS collation; by is matched to
column names case-insensitively, and variables named in descending are
sorted in reverse.
Usage
sas_sort(df, by, descending = character(), ...)
Arguments
df |
A data frame. |
by |
Character vector of BY variables. |
descending |
Character vector of those BY variables to sort descending. |
... |
Ignored. |
Value
df reordered.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sum()
Examples
sas_sort(data.frame(id = c(2, NA, 1)), by = "id")
sas_sort(data.frame(id = c(2, NA, 1)), by = "id", descending = "id")
Row-wise SUM and MEAN with SAS missing semantics
Description
SAS SUM() and MEAN() are row-wise across their arguments: each
observation gets the sum or mean of its non-missing arguments, and only an
observation whose arguments are all missing yields missing. Column
aggregation (PROC MEANS, PROC SQL) is emitted inline by those emitters
and never routed through these.
Usage
sas_sum(...)
sas_mean(...)
Arguments
... |
Numeric vectors, recycled to a common length. |
Value
A numeric vector of that length.
See Also
Other runtime helpers:
apply_format(),
chr_cmp(),
lib_delete(),
lib_exists(),
lib_members(),
lib_read(),
sas2r_fold_names(),
sas2r_libname_assign(),
sas2r_resolve_registry(),
sas2r_source_include(),
sas_compress(),
sas_display(),
sas_if_else(),
sas_merge(),
sas_min(),
sas_missing(),
sas_round(),
sas_sort()
Examples
sas_sum(c(1, NA, 2), c(1, NA, NA))
sas_mean(c(1, NA, 3), c(3, NA, NA))
Translate SAS programs or project to R with dependency-aware orchestration
Description
Exposes the single authoritative SAS-to-R migration pipeline. SAS source is the only required input. Operates on a single SAS file (wrapped as a one-module project) or a directory of SAS files with identical internal sequencing: initializes/reconciles run -> discovers dependency graph and output contracts -> baseline generation and immediate program repair -> full bundle attempt execution and output-driven repair -> deterministic gate selection and reporting.
Usage
sas_translate(
path,
out_dir = NULL,
config = NULL,
execute = TRUE,
max_program_repair_rounds = 1L,
max_bundle_repair_rounds = NULL,
outputs = NULL,
agent_evidence = c("code_only", "bounded"),
llm = NULL,
budget_usd = Inf,
budget_mode = "stop",
pricing_source = "catalog",
pricing_rates = NULL,
usage_limits = NULL,
recursive = FALSE,
resume = FALSE,
keep_raw_attempts = FALSE,
max_bundle_repairs_per_component = 2L,
max_parallel_translations = NULL
)
Arguments
path |
Path to a SAS file or directory containing SAS files, or a |
out_dir |
Output directory path for generated R bundle, attempts, and reports. Defaults to a temporary directory.
Relative paths resolve from the calling working directory before workers start.
Source parsing caches use the R session's temporary directory, not the input
project. Wrap the call in |
config |
Optional configuration list, YAML path, or |
execute |
Logical; whether to run meaningful program smoke and full bundle execution. Defaults to TRUE. |
max_program_repair_rounds |
Maximum repair rounds per component in immediate loop. Defaults to 1L. |
max_bundle_repair_rounds |
Optional overall cap on bundle fixer calls. Defaults to NULL, allowing each component its own bounded repair allowance. An explicit zero disables bundle repair. |
outputs |
Optional character vector or list specifying output contract overrides. Reference paths supplied here resolve from the calling working directory. Output overrides are retained in the returned project for reuse. |
agent_evidence |
Agent evidence policy ("code_only" or legacy "bounded"). Both exclude record previews and raw runtime messages. Focused investigation may receive approved aggregate patterns; fixers remain reference-blind. |
llm |
Optional |
budget_usd |
Dollar threshold for LLM spend. Defaults to Inf (unlimited). |
budget_mode |
Dollar enforcement mode: "stop", "observe", "soft", or "strict". Defaults to "stop". |
pricing_source |
Cost provenance source ("catalog", "adapter", "organization", "external"). Defaults to "catalog". |
pricing_rates |
Optional organization pricing table. |
usage_limits |
Optional named list of non-dollar request limits:
|
recursive |
Logical; whether to scan subdirectories recursively. Defaults to FALSE. |
resume |
Logical; reuse saved translation revisions and completed reviews when source, input data, configuration, helpers, and worker prompts still match. Full-bundle execution and output checks use fresh attempts; passing or deferred smoke results can be reused when their full context matches. Changing only the parallel translation limit does not invalidate reuse. Missing or edited revision files trigger regeneration. Compatible version-8 checkpoints are imported; other incompatible checkpoints regenerate with an explanation before new provider calls. Defaults to FALSE. |
keep_raw_attempts |
Logical; retain raw outputs from unselected attempts, including isolated component smoke outputs and their replay scripts. These are partial debugging artifacts, not validated final outputs. Defaults to FALSE. |
max_bundle_repairs_per_component |
Maximum bundle fixer calls per component across the run. Defaults to 2L; separate from immediate program repairs. |
max_parallel_translations |
Maximum concurrent SAS program or macro translation workflows. Overrides
|
Details
Missing inputs, called macros, includes and uncertain dependencies are reported
while available source continues translating. Unavailable dependencies defer
affected execution; they do not authorize inventing missing source or data.
Component failures preserve useful work and allow other components to proceed.
Unusable configuration, inconsistent pipeline coverage and critical provider or
accounting failures still stop the run. Use sas_preflight() to inspect
readiness and configure macros.search_path for external macro definitions.
Value
An object of S3 class "sas2r_translation" containing $run_id, $out_dir,
$bundle_dir, $outputs_dir, $status, $status_reason, $graph_path,
$output_contracts_path, $report_path, $report_json_path, $component_evidence,
$output_assessments, $diagnostics, $repair_history, $usage, and $project.
$outputs_dir contains selected contract deliverables with their library and
relative-path layout (for example datasets/work/out.rds,
datasets/adam/adsl.rds, and tlf/outputs/table.html); it is NULL when
execution is disabled or no selected deliverables were produced. The JSON
report includes separate target/reference/review coverage and effective limits.
Examples
# Translate a small SAS program with the deterministic rule-based engine.
# Neither an LLM nor a SAS installation is required.
sas_dir <- file.path(tempdir(), "sas2r-translate-example")
dir.create(sas_dir, showWarnings = FALSE)
writeLines(c(
"data work.adsl;",
" set adam.dm;",
" where age >= 18;",
" bmi = weight / (height * height);",
"run;",
"",
"proc sort data=work.adsl out=work.adsl_sorted;",
" by usubjid;",
"run;"
), file.path(sas_dir, "adsl.sas"))
# execute = FALSE translates without running the generated bundle.
res <- sas_translate(sas_dir, out_dir = tempfile("sas2r-out"), execute = FALSE)
res$status
cat(sas_code(res))
## Not run:
# Run the generated bundle and let the agents translate, review, and repair
# the patterns the deterministic rules cannot prove.
llm <- sas_llm(list(provider = "anthropic", model = "claude-sonnet-4-6"))
res <- sas_translate("path/to/sas/project", llm = llm, recursive = TRUE)
sas_write(res, "path/to/r/bundle")
## End(Not run)
Write translated code artifacts and outputs to a destination directory
Description
Copies the selected editable bundle (including failed code), with its
programs/, macros/, runtime/, autoexec.R, run.R, and README.
Saved automated deliverables go to saved-outputs/ and reports to report/.
Existing manual output/ files are not copied; configuration edits are kept.
Input libraries remain external dependencies documented in the README.
Run Rscript run.R from the exported folder, or source("run.R", chdir = TRUE).
Warns if status is blocked or needs_review.
Usage
sas_write(x, dir, overwrite = FALSE)
Arguments
x |
A |
dir |
Target directory path to write translated files. |
overwrite |
Replace an existing sas2r export. Non-export directories must be empty. Replacing an export updates only its previously listed files; unrelated files are preserved and conflicting new paths are refused. This also replaces your edits to previously exported files. Export to a new directory to preserve an edited bundle; edits are not merged or backed up. |
Value
The target directory path, invisibly.
Examples
sas_dir <- file.path(tempdir(), "sas2r-write-example")
dir.create(sas_dir, showWarnings = FALSE)
writeLines(c("proc sort data=work.dm out=work.dm_sorted;",
" by usubjid;",
"run;"), file.path(sas_dir, "sort.sas"))
res <- sas_translate(sas_dir, out_dir = tempfile("sas2r-out"), execute = FALSE)
# Writing an unverified translation warns; the bundle is still written.
dest <- tempfile("sas2r-bundle")
suppressWarnings(sas_write(res, dest))
list.files(dest, recursive = TRUE)
Split a SAS dataset name into its library and member
Description
This internal helper is available in a generated bundle's runtime.
Use the returned names exactly; it returns a character vector, not a list.
Accessing an absent name with [[ raises an error, rather than returning NULL.
Usage
split_ds(ds, macro_vars = character())
Arguments
ds |
A character scalar such as |
macro_vars |
Named character vector of macro variables to substitute before splitting, without the leading ampersand in each name. |
Value
A named character vector of length two, with elements lib and
member. A one-level name uses lib = "work". There is no libref element.
Write a PROC-COMPARE-style markdown report or versioned JSON comparison report
Description
Write a PROC-COMPARE-style markdown report or versioned JSON comparison report
Usage
write_comparison_report(
x,
file = NULL,
base_label = "SAS (base)",
comp_label = "R (compare)",
run_id = "default",
dir = NULL,
...
)
Arguments
x |
An object of class |
file |
Path to the output markdown or JSON file. |
base_label |
Label for the base dataset. |
comp_label |
Label for the compare dataset. |
run_id |
Run ID for output review directory structure. |
dir |
Base directory for |
... |
Additional options. |
Value
Path to the written file, invisibly.
Examples
sas <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.0))
r <- data.frame(usubjid = c("01-001", "01-002"), aval = c(1.0, 2.5))
cmp <- compare_datasets(sas, r, keys = "usubjid")
# PROC COMPARE-style markdown.
md <- file.path(tempdir(), "sas2r-comparison.md")
write_comparison_report(cmp, file = md)
cat(head(readLines(md), 6), sep = "\n")