# Tests for import/module edge cases # Covers circular imports, module caching, error cleanup test_that("import non-existent module fails gracefully", { engine <- make_engine() expect_error( engine$eval_text('(import "non_existent_module_12345")'), "not found|import|load|module" ) }) test_that("import with invalid path", { 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)", { 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", { # 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", { # 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", { # 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", { # 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", { # 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", { # 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", { 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", { 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", { engine <- make_engine() expect_error( engine$eval_text("(import-runtime some-mod)"), "reserved for future use" ) }) test_that("load with relative path", { # 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", { 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", { 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", { 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", { 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", { 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") })