tests/testthat/test-import-edge-cases.R

# Tests for import/module edge cases
# Covers circular imports, module caching, error cleanup

thin <- make_cran_thinner()

test_that("import non-existent module fails gracefully", {
  thin()
  engine <- make_engine()

  expect_error(
    engine$eval_text('(import "non_existent_module_12345")'),
    "not found|import|load|module"
  )
})

test_that("import with invalid path", {
  thin()
  engine <- make_engine()

  expect_error(
    engine$eval_text('(import "/invalid/path/to/module")'),
    "not found|import|load|module"
  )
})

test_that("same file imported with different path strings uses one module (absolute path alias)", {
  thin()
  engine <- make_engine()
  env <- engine$get_env()

  tmp_dir <- tempfile()
  dir.create(tmp_dir)
  old_dir <- getwd()
  setwd(tmp_dir)
  on.exit({
    setwd(old_dir)
    unlink(tmp_dir, recursive = TRUE)
  }, add = TRUE)

  module_file <- file.path(tmp_dir, "aliasm.arl")
  writeLines(c(
    "(module aliasm",
    "  (export getn)",
    "  (define n 0)",
    "  (set! n (+ n 1))",
    "  (define getn (lambda () n)))"
  ), module_file)

  path_abs <- normalizePath(module_file, winslash = "/", mustWork = TRUE)
  path_rel <- "aliasm.arl"

  engine$eval(engine$read(sprintf('(import "%s" :refer :all)', path_abs))[[1]], env = env)
  n_after_first <- engine$eval(engine$read("(getn)")[[1]], env = env)

  engine$eval(engine$read(sprintf('(import "%s" :refer :all)', path_rel))[[1]], env = env)
  n_after_second <- engine$eval(engine$read("(getn)")[[1]], env = env)

  expect_equal(n_after_first, 1L)
  expect_equal(n_after_second, 1L)
})

test_that("module is cached after first load", {
  thin()
  # Create a temporary module file
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_cache_module.arl")
  writeLines('(define cached-var 42)', module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()
  env <- toplevel_env(engine)

  # First load
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  first_result <- engine$eval_text("cached-var")
  expect_equal(first_result, 42)

  # Modify the file
  writeLines('(define cached-var 99)', module_path)

  # Second load - should still get cached value
  # (Note: actual caching behavior depends on implementation)
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
})

test_that("load returns last value from module", {
  thin()
  # Create a temporary module
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_return_module.arl")
  writeLines(c(
    '(define x 10)',
    '(define y 20)',
    '(+ x y)'
  ), module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()
  result <- engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))

  # Should return 30 (the last expression)
  expect_equal(result, 30)
})

test_that("load with syntax error in module", {
  thin()
  # Create a module with syntax error
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_syntax_error.arl")
  writeLines(c(
    '(define x 10',  # Unclosed paren
    '(+ x 5)'
  ), module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()

  expect_error(
    engine$eval_text(sprintf('(load "%s")', arl_path(module_path))),
    "Unclosed|parse|syntax|EOF|incomplete"
  )
})

test_that("load with runtime error in module", {
  thin()
  # Create a module that errors at runtime
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_runtime_error.arl")
  writeLines(c(
    '(define x 10)',
    '(stop "deliberate runtime error")'
  ), module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()

  expect_error(
    engine$eval_text(sprintf('(load "%s")', arl_path(module_path))),
    "deliberate runtime error"
  )
})

test_that("multiple loads of same module", {
  thin()
  # Create a simple module
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_multiple_load.arl")
  writeLines('(define multi-load-var 123)', module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()

  # Load multiple times
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))

  result <- engine$eval_text("multi-load-var")
  expect_equal(result, 123)
})

test_that("load can access previously defined symbols", {
  thin()
  # Create a module that uses predefined symbols
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_access_symbols.arl")
  writeLines('(+ existing-x 10)', module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()
  env <- toplevel_env(engine)

  # Define a symbol first
  engine$eval_text("(define existing-x 5)")

  # Load module that uses it
  result <- engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  expect_equal(result, 15)
})

test_that("nested module loads", {
  thin()
  temp_dir <- tempdir()

  # Create module B
  module_b_path <- file.path(temp_dir, "test_module_b.arl")
  writeLines('(define b-var 20)', module_b_path)
  on.exit(unlink(module_b_path), add = TRUE)

  # Create module A that loads B
  module_a_path <- file.path(temp_dir, "test_module_a.arl")
  writeLines(c(
    sprintf('(load "%s")', arl_path(module_b_path)),
    '(define a-var (+ b-var 10))'
  ), module_a_path)
  on.exit(unlink(module_a_path), add = TRUE)

  engine <- make_engine()
  env <- toplevel_env(engine)

  # Load module A (which loads B)
  engine$eval_text(sprintf('(load "%s")', arl_path(module_a_path)))

  # Should have both variables
  result_a <- engine$eval_text("a-var")
  expect_equal(result_a, 30)

  result_b <- engine$eval_text("b-var")
  expect_equal(result_b, 20)
})

test_that("circular import dependency produces clear error", {
  thin()
  temp_dir <- tempfile()
  dir.create(temp_dir)
  on.exit(unlink(temp_dir, recursive = TRUE), add = TRUE)
  old_wd <- getwd()
  setwd(temp_dir)
  on.exit(setwd(old_wd), add = TRUE)

  # Module A imports B, Module B imports A
  writeLines(c(
    "(module circ-a",
    "  (export a-fn)",
    "  (import circ-b)",
    "  (define a-fn (lambda () 1)))"
  ), file.path(temp_dir, "circ-a.arl"))

  writeLines(c(
    "(module circ-b",
    "  (export b-fn)",
    "  (import circ-a)",
    "  (define b-fn (lambda () 2)))"
  ), file.path(temp_dir, "circ-b.arl"))

  engine <- make_engine()
  expect_error(
    engine$eval_text("(import circ-a)"),
    "Circular dependency detected: circ-a -> circ-b -> circ-a"
  )
})

test_that("import-runtime is reserved and errors", {
  thin()
  engine <- make_engine()

  expect_error(
    engine$eval_text("(import-runtime some-mod)"),
    "reserved for future use"
  )
})

test_that("load with relative path", {
  thin()
  # Create a module in current directory
  temp_dir <- tempdir()
  old_wd <- getwd()
  setwd(temp_dir)
  on.exit(setwd(old_wd), add = TRUE)

  module_path <- "test_relative.arl"
  writeLines('(define relative-var 777)', module_path)
  on.exit(unlink(file.path(temp_dir, module_path)), add = TRUE)

  engine <- make_engine()
  env <- toplevel_env(engine)

  result <- engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  var_result <- engine$eval_text("relative-var")
  expect_equal(var_result, 777)
})

test_that("load empty module", {
  thin()
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_empty.arl")
  writeLines('', module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()

  # Loading empty module should succeed and return NULL or similar
  result <- engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  # Result behavior may vary - just ensure no crash
  expect_no_error(result)
})

test_that("load module with only comments", {
  thin()
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_comments_only.arl")
  writeLines(c(
    '; This is a comment',
    '; Another comment',
    '; ; More comments'
  ), module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()

  # Should succeed
  result <- engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))
  expect_no_error(result)
})

test_that("module defines macro", {
  thin()
  temp_dir <- tempdir()
  module_path <- file.path(temp_dir, "test_macro_module.arl")
  writeLines(c(
    '(defmacro triple (x) `(* 3 ,x))'
  ), module_path)
  on.exit(unlink(module_path), add = TRUE)

  engine <- make_engine()
  env <- toplevel_env(engine)

  # Load module with macro
  engine$eval_text(sprintf('(load "%s")', arl_path(module_path)))

  # Use the macro
  result <- engine$eval_text("(triple 7)")
  expect_equal(result, 21)
})

# ============================================================================
# Windows backslash path regression tests
# ============================================================================

test_that("Windows-style backslash paths work when properly normalized", {
  thin()
  engine <- make_engine()

  # Create a temp file in a directory whose name would trigger escape issues
  # if backslashes weren't normalized (e.g. contains 't', 'n', 'r' after separator)
  tmp <- tempfile(pattern = "test_path")
  writeLines("(define win-path-test 42)", tmp)
  on.exit(unlink(tmp))

  # arl_path should convert backslashes to forward slashes
  safe <- arl_path(tmp)
  expect_false(grepl("\\\\", safe))

  # Engine should successfully load via the normalized path
  result <- engine$eval_text(sprintf('(load "%s")', safe))
  expect_equal(engine$eval_text("win-path-test"), 42)
})

test_that("backslash escapes in Arl strings produce correct characters", {
  thin()
  engine <- make_engine()

  # Verify \t, \n, \\ are processed as escape sequences (standard behavior)
  expect_equal(engine$eval_text('"hello\\tworld"'), "hello\tworld")
  expect_equal(engine$eval_text('"hello\\nworld"'), "hello\nworld")
  expect_equal(engine$eval_text('"back\\\\slash"'), "back\\slash")
})

Try the arl package in your browser

Any scripts or data that you put into this service are public.

arl documentation built on March 19, 2026, 5:09 p.m.