Dependency-Aware SAS-to-R Migration

Songyue Chen

2026-10-21

Overview

sas2r provides a dependency-aware migration framework that translates SAS programs and multi-module clinical pipelines into modular, reproducible R bundles.

sas2r analyzes macro definitions, %include relationships, dataset lineages, and configured outputs to schedule programs in dependency order. Unresolved dependencies and unsupported source constructs remain visible in the report.

Migration Pipeline Architecture

A complete migration workflow follows six coordinated phases:

  1. Dependency analysis and scheduling: Discovers module dependencies, macro definitions and data references. A deterministic coordinator assigns dependency-ready programs and called macros; cycles and unresolved dependencies remain findings.
  2. Translation and initial review: Builds a deterministic baseline, hands unsupported source to the configured translator, checks the candidate and obtains independent review. Independent components can overlap in separate R processes when parallel mode is enabled.
  3. Immediate checks and repair: Runs smoke execution and bounded source-grounded repairs one component at a time, including re-review and checks of affected consumers. Dependents wait until their providers finish this processing.
  4. Final review checkpoint: Holds the selected code and helpers fixed, reuses reviews whose context still matches, and reviews remaining components. These reviews can overlap but do not repair code during the pass.
  5. Bundle execution and output checks: Runs the complete generated bundle in dependency order inside an isolated subprocess using copy-on-write library registries. It checks required datasets and TLFs, compares configured dataset references, and runs bounded source-grounded repair followed by fresh execution. This phase remains serial.
  6. Authoritative reporting: Produces machine-readable (report/report.json) and human-readable (report/translation.md) evidence, including unavailable checks and unresolved findings.

The coordinator owns selected revisions, checkpoints and the shared usage budget. Each process uses the existing translator, reviewer and fixer context builders, tools and settings with a snapshot of the selected dependencies. Processes report results to the coordinator; they do not share a live conversation. In parallel mode, an unresolved dependency finding defers its component and known descendants while independent work continues. Required deferred execution or incomplete review prevents the run from becoming migration_ready or validated; inspect the reported status and reason for the affected components.

Configuring a Clinical Study Migration (_sas2r.yml)

Create a _sas2r.yml file in your clinical study directory:

project: cdsc_pilot_study

libraries:
  sdtm:
    path: data/sdtm
    engine: xpt
  adam:
    path: data/adam
    engine: xpt
    write: rds

outputs:
  datasets:
    - adam.adsl
    - adam.adae
    - adam.advs_bds

  # Golden SAS reference datasets for automated validation
  references:
    adam.adsl: data/reference/adsl.xpt
    adam.adae: data/reference/adae.sas7bdat
    adam.advs_bds: data/reference/advs.rds

  assertions:
    adam.adsl:
      keys: [STUDYID, USUBJID]
      numeric_tolerance: 1e-6
    adam.advs_bds:
      keys: [STUDYID, USUBJID, PARAMCD, AVISITN, ATPTN]
      numeric_tolerance: 1e-6

llm:
  provider: gemini              # or deepseek, openai, anthropic
  model: gemini-3.8-flash
  reasoning_effort: high
  max_output_tokens: 65536
  timeout_seconds: 900
  max_tries: 1
  capabilities:
    tool_calling: native
    structured_output: fallback

max_parallel_translations counts concurrent program or macro workflows, including their reviews and repairs. It does not allocate one slot per agent role or per CPU. Establish a baseline at 1, then evaluate 2 against the same source, inputs and quality checks. Higher values depend on provider capacity and local memory; local execution and repair remain serial. Set it with the sas_translate() argument max_parallel_translations. Parallel mode is opt-in while live quality and performance validation remains outstanding.

Gemini Flash and DeepSeek Flash are suggested starting points for evaluation; stronger frontier models remain available. Choose using source review and output evidence for the actual study. See the provider settings guide for provider-specific output limits, timeouts and capabilities. The adapter preserves native provider conversation history through tool calls. Offline replay tests cover tool continuation and parallel workers on ellmer 0.5.0; they do not establish live-model translation quality or study-level accuracy.

Inspect the setup offline

library(sas2r)
migration_dir <- tempfile("sas2r-migration-")
check <- sas_preflight("programs/",   # the folder of SAS programs to translate
                       config = "_sas2r.yml",
                       out_dir = migration_dir, diagnose = "off")
print(check)
check$libraries
check$inputs
check$unsupported
stopifnot(check$model_calls == 0L)

This reads source files and directory listings without model calls, dataset reads, program execution, or output writes. The program folder is the path argument, not a configuration key; a single .sas file also works, and recursive = TRUE includes subfolders. Reuse check$project for translation while sources remain unchanged. migration.execution_order can name all executable root programs in their intended shared-session order; each WORK read then uses the latest preceding completed write. Included programs execute at their call sites. When the order is omitted, dependency inference remains the default. With diagnose = "auto", configured AI can suggest corrections for findings in one bounded request; advice never overrides static checks. Use qc_profile() with outputs$profiles and a named profile in each target’s assertions to require labels, formats, types/order, keys, uniqueness, row counts, and per-variable reference tolerances. These checks add study requirements; passing them alone does not establish SAS reference agreement.

Declare a shared-session program order

This complete offline example inspects three programs. The second program reads the first version of work.tmp, then replaces it. The third reads that replacement.

ordered_study <- tempfile("sas2r-ordered-study-")
dir.create(ordered_study)
writeLines("data work.tmp; value=1; run;",
           file.path(ordered_study, "create.sas"))
writeLines("data work.tmp; set work.tmp; value=value+1; run;",
           file.path(ordered_study, "replace.sas"))
writeLines("data work.final; set work.tmp; run;",
           file.path(ordered_study, "read.sas"))
ordered_config <- list(migration = list(
  execution_order = c("create.sas", "replace.sas", "read.sas")
))
ordered_check <- sas_preflight(ordered_study, config = ordered_config,
                               diagnose = "off")
ordered_check$pipeline$execution_order
ordered_check$inputs[c("dataset", "status")]
stopifnot(ordered_check$pipeline$order_source == "configured",
          all(ordered_check$inputs$status == "generated"),
          ordered_check$model_calls == 0L)
unlink(ordered_study, recursive = TRUE)

For an R configuration list, these paths are relative to the scanned project directory. In YAML they are relative to the configuration file. List every root program exactly once; called macros, startup and includes keep their existing roles. The order does not change which files path and recursive select. Rescan the sources when changing an order on a previously scanned project.

Explicit order applies to translation, smoke checks and final execution. Ordered roots translate sequentially, and smoke checks replay the preceding session. Unknown macro effects can still leave state deferred. A later writer does not make an earlier absent permanent input available. Omitting the order retains dependency inference.

Running a Migration in R

To migrate a single SAS script or an entire directory of SAS files, call sas_translate(). These examples write under the R session temporary directory; supply your own destination when you want to retain the files.

library(sas2r)

# Load API keys from environment
readRenviron("~/.Renviron")

# Translate and execute the study migration
result <- sas_translate(
  path = check$project,
  out_dir = migration_dir,
  max_parallel_translations = 1L,   # programs or macros translated at once
  execute = TRUE,
  max_program_repair_rounds = 1L,
  max_bundle_repairs_per_component = 2L,
  max_bundle_repair_rounds = NULL
  # Optional limits, commented out by default. Uncomment to enforce them; the
  # run stops admitting requests when a limit is reached. Requests per program vary by model
  # and study; size ceilings from a completed run's usage summary.
  # Catalog costs are estimates; an in-flight request may exceed the threshold.
  # , budget_usd = 10, budget_mode = "soft"
  # , usage_limits = list(max_calls = 700, max_tool_calls = 700, max_wall_time = 14400)
)

# Inspect the result object
print(result)
cat("Status:", result$status, "\n")
cat("Bundle Directory:", result$bundle_dir, "\n")
cat("Outputs Directory:", result$outputs_dir, "\n")
cat("Report:", result$report_path, "\n")

# View generated R code
cat(sas_code(result))

# Export selected code, generated outputs, reports, and the dependency/input guide
export_dir <- tempfile("sas2r-export-")
sas_write(result, export_dir)

Outputs preserve library subdirectories: ADSL is at file.path(result$outputs_dir, "datasets", "adam", "adsl.rds"). The export contains a dependency-ordered run.R, run-order.json, and a README with run/edit/move instructions. Programs and macros are in programs/ and macros/; saved automated outputs are under saved-outputs/, separate from manual writes to output/. From the export directory, use Rscript run.R or source("run.R", chdir = TRUE). Install the R dependencies listed in its guide and update input paths in autoexec.R and explicit source LIBNAME statements when inputs move. Input datasets are external dependencies; the exported report describes the original selected execution and is not refreshed by a manual rerun.

Repeat the same migration call with resume = TRUE to reuse saved revisions and completed reviews when source, inputs, configuration, runtime, and prompts still match. Changing only the parallel translation limit does not invalidate reuse. Missing or changed artifacts regenerate and unavailable reviews are retried. Passing or deferred smoke results can be reused when their full context matches; full-bundle execution and output checks use fresh attempts. Parallel checkpoints distinguish drafts from components that have completed immediate processing, and save final reviews individually. Repair and recorded revisit counts survive resume. The usage ledger remains cumulative across resumed runs. Every limit is commented out by default so a run finishes without interruption; uncomment budget_usd or a usage_limits ceiling only if you want the run to stop when it is reached; requests per program vary by model and study, so size a ceiling from a completed run’s usage summary. Unknown-cost calls must not be read as zero spend.

Four Canonical Bundle Statuses

Each translation attempt resolves to one of four authoritative statuses:

Inspect coverage as well as status. The reports separate produced, reference-compared, passed, and reference-passed outputs and list validated_targets and unreferenced_targets. A validated bundle is not a claim that every output was compared.

Review completion and reference comparability

reviewer ...: ok means the agent call completed. Its semantic verdict is in the review record; coordinator ...: reviewed can still be followed by repair. Mechanical checks and smoke tests can pass while the reviewer finds a logic issue. A new repaired revision is checked and reviewed again.

Reference datasets must correspond to the supplied SAS source, input snapshot, formats, macros, and parameters. Different subject inclusion rules, KEEP lists, or analysis visit definitions can produce discrepancies even when the generated R follows the source. Trace differences to the source and inputs before treating them as translation errors. Independent R reconstruction is useful diagnostic evidence but is not an execution by SAS.

Component Evidence Ladder

Within each bundle, individual components begin without an evidence level and accumulate evidence bound to their revision:

Smoke outcomes are recorded separately. A smoke pass with unavailable review does not earn a higher evidence level. Reference provenance is established separately from the comparison result.

Attempt Isolation & Copy-on-Write

The library helpers separate input reads from generated writes during translation:

Model Privacy & Clinical Data Protection

sas2r keeps reference comparison answers out of translation and repair agents:

Parity Disclaimer

THIS IS NOT PARITY: Automated translation by sas2r does not guarantee regulatory equivalence or replace independent statistical validation. All migrated R code and generated artifacts must be formally validated according to standard clinical operating procedures prior to regulatory submission.