Summary
let-go does not evaluate a top-level (do ...) subform-by-subform the way Clojure does. A top-level do is compiled as a single unit, so a namespace-qualified symbol in a later subform is resolved at compile time — before an earlier subform's require has run. This breaks the common (do (require 'x) (x/f)) idiom (and any top-level do where a later form depends on the compile-time effect of an earlier one), and it diverges from Clojure.
Repro (zero dependencies, two files)
a.lg:
(ns a)
(defn hello [] :hi)
# a) do-wrapped: FAILS
$ lg -e "(do (require 'a) (a/hello))"
error: Can't resolve a/hello in this context
--> EXPR:1:18
|
1 | (do (require 'a) (a/hello))
| ^^^
# b) the SAME two forms, un-wrapped (separate top-level forms): WORKS
$ printf "(require 'a)\n(println (a/hello))\n" > run.lg && lg run.lg
:hi
So it is specifically the do wrapper that forces single-unit compilation; let-go already evaluates separate top-level forms sequentially (run.lg works), it just doesn't extend that to the subforms of a top-level do.
Note: clojure.* stdlib namespaces are embedded and resolve at compile time without require, so they mask this bug — (do (require 'clojure.set) (clojure.set/union #{1} #{2})) returns #{1 2} even though (find-ns 'clojure.set) is still false. You must use a user namespace loaded from disk (like a above) to reproduce.
Expected (Clojure behavior)
Clojure's eval special-cases a top-level do: clojure.lang.Compiler.eval checks whether the form is a seq beginning with do and, if so, recursively evaluates each subform in turn (compile + eval one, then the next), returning the last. So earlier subforms' effects (a require loading a namespace, an in-ns, an intern) are visible when later subforms are compiled. This is also why a macro may expand into (do (defines-X ...) (uses X ...)) at the top level.
;; Clojure (and babashka) — works:
(do (require '[clojure.string :as s]) (s/upper-case "hi")) ;=> "HI"
let-go gives Can't resolve s/upper-case in this context for the user-namespace equivalent.
Impact
- REPL / nREPL ergonomics: require-then-use in one submission must be split into separate top-level forms;
(do (require ...) (use ...)) — the natural thing to type/send — fails. (Hit this driving the let-go nREPL from an MCP tool.)
- Macros: a macro that expands to a top-level
(do ...) whose later forms depend on an earlier form's compile-time effect won't work the way it does in Clojure.
Workaround
Submit the forms as separate top-level forms (not wrapped in do, let, when, or a fn body — those fail in both let-go and Clojure since they're genuinely one compiled unit). The only thing missing in let-go is Clojure's top-level-do special case.
Version
lg 1.9.0 (9cb2167), macOS arm64.
Summary
let-go does not evaluate a top-level
(do ...)subform-by-subform the way Clojure does. A top-leveldois compiled as a single unit, so a namespace-qualified symbol in a later subform is resolved at compile time — before an earlier subform'srequirehas run. This breaks the common(do (require 'x) (x/f))idiom (and any top-leveldowhere a later form depends on the compile-time effect of an earlier one), and it diverges from Clojure.Repro (zero dependencies, two files)
a.lg:So it is specifically the
dowrapper that forces single-unit compilation; let-go already evaluates separate top-level forms sequentially (run.lg works), it just doesn't extend that to the subforms of a top-leveldo.Expected (Clojure behavior)
Clojure's
evalspecial-cases a top-leveldo:clojure.lang.Compiler.evalchecks whether the form is a seq beginning withdoand, if so, recursively evaluates each subform in turn (compile + eval one, then the next), returning the last. So earlier subforms' effects (arequireloading a namespace, anin-ns, anintern) are visible when later subforms are compiled. This is also why a macro may expand into(do (defines-X ...) (uses X ...))at the top level.let-go gives
Can't resolve s/upper-case in this contextfor the user-namespace equivalent.Impact
(do (require ...) (use ...))— the natural thing to type/send — fails. (Hit this driving the let-go nREPL from an MCP tool.)(do ...)whose later forms depend on an earlier form's compile-time effect won't work the way it does in Clojure.Workaround
Submit the forms as separate top-level forms (not wrapped in
do,let,when, or a fn body — those fail in both let-go and Clojure since they're genuinely one compiled unit). The only thing missing in let-go is Clojure's top-level-dospecial case.Version
lg 1.9.0 (9cb2167), macOS arm64.