--- title: "Troubleshooting" output: arl::arl_html_vignette pkgdown: as_is: true vignette: > %\VignetteIndexEntry{Troubleshooting} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") arl::register_knitr_engine() ``` Common pitfalls and how to resolve them. ## Zero is falsy Unlike many Lisps, Arl treats `0` as falsy (along with `#f`/`FALSE`, `#nil`/`NULL`). This matches R's convention where `0` is equivalent to `FALSE`. ```{arl} (if 0 "truthy" "falsy") ; => "falsy" (if 1 "truthy" "falsy") ; => "truthy" (when 0 (print "nope")) ; => #nil ``` ```{arl, include=FALSE} (assert-equal "falsy" (if 0 "truthy" "falsy")) (assert-equal "truthy" (if 1 "truthy" "falsy")) (assert-equal #nil (when 0 "nope")) ``` If you need to distinguish zero from false, test explicitly: ```{arl, eval=FALSE} (if (not (nil? x)) ...) ; check for nil/NULL (if (number? x) ...) ; check for any number ``` ## `list?` vs `base::is.list` on quoted forms Under the hood, quoted forms are R [language/call objects](https://cran.r-project.org/doc/manuals/r-devel/R-lang.html#Computing-on-the-language), not R lists. Arl still treats calls as list-like for Lisp semantics, so these can disagree: ```{arl} (list? '(1 2 3)) ; => #t (base::is.list '(1 2 3)) ; => #f ``` ```{arl, include=FALSE} (assert-true (list? '(1 2 3))) (assert-false (base::is.list '(1 2 3))) ``` Use `list?` when writing Arl code that follows Lisp list semantics. Use `base::is.list` only when you specifically need R's underlying object type. ## Use `#t` / `#f`, not `T` / `F` In R, `T` and `F` are ordinary variables (not reserved words) that happen to be bound to `TRUE` and `FALSE` by default. In Arl, use the literal boolean syntax instead: ```{arl} ;; Correct (if #t "yes" "no") (define flag #f) ;; Fragile -- T/F can be rebound (define T 42) (if T "oops" "no") ; => "oops" (42 is truthy) ``` ```{arl, include=FALSE} (assert-equal "yes" (if #t "yes" "no")) (assert-equal "oops" (if T "oops" "no")) ``` ## `load` vs `import` vs `run` These three forms load code differently: | Form | Scope | Definitions visible? | Use case | |------|-------|---------------------|----------| | `(load "file.arl")` | Current env | Yes | Source a script into REPL | | `(load "file.arl" env)` | `env` | Yes | Source into a chosen environment | | `(import module)` | Current scope | Exports only | Use a module's public API | | `(run "file.arl")` | Isolated child | No | Execute a script without binding side effects | From R: - `engine$load_file_in_env(path)` -- definitions visible (like `load`) - `engine$load_file_in_env(path, new.env(parent = env))` -- isolated scope (like `run`) If you get "symbol not found" after loading a file, you may have used `run` or an isolated child environment when you needed `load` or `engine$load_file_in_env()`. ## Self-TCO limitations The compiler's self-tail-call optimization applies to `(define name (lambda ...))` and `(set! name (lambda ...))` forms, which means `letrec`-bound lambdas are also covered. However, some patterns are **not** optimized: ```{arl, eval=FALSE} ;; TCO works here (define factorial (lambda (n acc) (if (< n 2) acc (factorial (- n 1) (* acc n))))) ;; TCO does NOT apply -- mutual recursion (each function calls the other, ;; not itself, so self-TCO cannot help) (define is-even? (lambda (n) (if (= n 0) #t (is-odd? (- n 1))))) (define is-odd? (lambda (n) (if (= n 0) #f (is-even? (- n 1))))) ;; This will overflow the stack for large n (is-even? 100000) ``` ```{arl, include=FALSE} (define __tco-fact (lambda (n acc) (if (< n 2) acc (__tco-fact (- n 1) (* acc n))))) (assert-equal 120 (__tco-fact 5 1)) ``` For mutual recursion, anonymous lambdas, or other cases where self-TCO does not apply, use `loop`/`recur` from the `looping` module: ```{arl, eval=FALSE} (loop ((i n) (acc 1)) (if (< i 2) acc (recur (- i 1) (* acc i)))) ``` ```{arl, include=FALSE} (assert-equal 120 (loop ((i 5) (acc 1)) (if (< i 2) acc (recur (- i 1) (* acc i))))) ``` ## Macro hygiene and `gensym` Arl macros are **hygienic by default** -- bindings introduced by a macro (via `define`, `let`, `lambda`, etc.) are automatically renamed so they cannot collide with names at the call site. This means simple macros work without any extra effort: ```{arl, eval=FALSE} ;; This is safe -- hygiene auto-renames 'tmp' so it won't ;; shadow a caller's variable named 'tmp' (defmacro my-swap (a b) `(let ((tmp ,a)) (set! ,a ,b) (set! ,b tmp))) ``` ```{arl, include=FALSE} (defmacro my-swap (a b) `(let ((tmp ,a)) (set! ,a ,b) (set! ,b tmp))) (define __sa 1) (define __sb 2) (my-swap __sa __sb) (assert-equal 2 __sa) (assert-equal 1 __sb) ``` You need `gensym` when you build binding forms **programmatically at expansion time** -- for example, computing a list of bindings in a loop before constructing the quasiquoted result. In that case, automatic hygiene cannot track the symbols because they are created outside the quasiquote template: ```{arl, eval=FALSE} ;; gensym needed: binding name is computed, not in quasiquote template (defmacro bind-all (pairs . body) (define temps (map (lambda (p) (gensym "t")) pairs)) `((lambda ,temps ,@body) ,@(map (lambda (p) (car (cdr p))) pairs))) ``` See [Macros and Quasiquote -- Hygiene](macros.html#hygiene) for a full explanation. ## Module not found errors When `(import name)` fails, check: 1. **Stdlib modules** are resolved from `inst/arl/` in the installed package. They are loaded automatically by the engine. 2. **User modules** are resolved relative to the current working directory. Make sure your file is named `name.arl` and is in the CWD. 3. **String imports** (`(import "path/to/file.arl")`) use path-only resolution -- no stdlib search. ## Stack traces with TCO-optimized functions When a TCO-optimized function errors, the stack trace shows only the outermost call frame because the recursive calls have been compiled into a loop. To debug: 1. Use `engine$inspect_compilation()` to see the compiled R code. 2. Temporarily add `(print ...)` statements inside the function body. 3. Or disable TCO temporarily with `Engine$new(disable_tco = TRUE)` or `options(arl.disable_tco = TRUE)` to get full stack traces during debugging. ## Related guides - [Getting Started](getting-started.html) - [Compiler and Internals](internals.html) - [Macros and Quasiquote](macros.html) - [Modules and Imports](modules.html)