From 63d92cd1224edbc605397d562e4c65bbfdd43a61 Mon Sep 17 00:00:00 2001 From: Dmitri Sotnikov Date: Wed, 10 Jun 2026 04:34:53 +0800 Subject: [PATCH] Stage2 task2 tier4b (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * core: Stage 2 Task 2 tier 4b — compile ns (+ use/import/refer-clojure) ns becomes a macro (00-syntax) expanding to (do (in-ns 'name) (require …) (use …) (import …) (refer-clojure …)) — matching Clojure, where require is a fn and ns expands to require calls. use/import/refer-clojure join the namespace ops as ctx-capturing clojure.core fns (use-impl/import-impl/ refer-clojure-impl, interned by install-stateful-fns!). Removed the ns interpreter special arm; dropped ns/use/import from host_iface special-names and loader stateful-head?. Placement matters: ns lives in the FIRST overlay tier (00-syntax), because the self-hosted analyzer build is triggered while 10-seq loads and processes jolt.analyzer's own (ns …) form — ns must exist by then. Its body resolves fn/map/reduce/cond at expansion time, by which point all of 00-syntax has loaded. The bootstrap compiler (compiler.janet) still PUNTS ns/in-ns/require/ use/import (it compiles analyzer.clj/ir.clj, whose ns forms must fall back to the interpreter that expands the macro) — only the self-hosted analyzer compiles them. use-impl fixes a latent bug: it now refers the used ns's vars into the CURRENT ns (the old special interned a ns into itself, a no-op). ns/use/import now compile + interpret as plain invokes. fallback-zero: ns off must-punt, onto must-compile. Gate green: conformance 267x3, fallback-zero 38/4, bootstrap-fixpoint stage1==2==3, self-host, staged-bootstrap, sci-bootstrap, clojure-test-suite >=4034/67, namespace, deps-loader, cli, aot, embedded-stdlib, uberscript, features 78/78, all unit + spec. * test/core: cover ns form + :use; fix use-impl spec parsing Checking test coverage for the namespace tiers surfaced a real bug nothing exercised: use-impl checked (array? s) to find a spec's ns symbol, but a vector spec like [src.x] is a pvec/tuple, not a Janet array — so (:use ...) and standalone (use ...) errored. Fixed to coerce (pvec->array) then take the head when the spec is indexed (matching require-impl). Added coverage that was missing: - conformance (x3 modes): the (ns …) form itself + :use refers. - namespaces-spec: (ns …) form, :use, standalone use. The :use/(use …) path had ZERO tests before, which is why the bug slipped through earlier tiers. EBNF needs no change — it's a reader/syntax grammar with no special-forms enumeration; ns/require/protocol syntax is unchanged (the destructuring note was already updated in jolt-f79). Gate green: conformance 269x3, fallback-zero 38/4, self-host, sci-bootstrap, bootstrap-fixpoint, staged-bootstrap, clojure-test-suite >=4034/67, namespace, all unit + spec (namespaces 27/27). --------- Co-authored-by: Yogthos --- jolt-core/clojure/core/00-syntax.clj | 25 +++++ src/jolt/compiler.janet | 2 +- src/jolt/evaluator.janet | 113 ++++++++++------------ src/jolt/host_iface.janet | 7 +- src/jolt/loader.janet | 2 +- test/integration/conformance-test.janet | 2 + test/integration/fallback-zero-test.janet | 3 +- test/spec/namespaces-spec.janet | 5 + 8 files changed, 90 insertions(+), 69 deletions(-) diff --git a/jolt-core/clojure/core/00-syntax.clj b/jolt-core/clojure/core/00-syntax.clj index 1b3131c..00ae85d 100644 --- a/jolt-core/clojure/core/00-syntax.clj +++ b/jolt-core/clojure/core/00-syntax.clj @@ -36,6 +36,31 @@ nil `(if ~(first clauses) ~(nth clauses 1) (cond ~@(drop 2 clauses))))) +;; ns is sugar over the namespace-op fns (in-ns/require/use/import/refer-clojure, +;; all ctx-capturing clojure.core fns) — matching Clojure, where require is a fn and +;; the ns macro expands its clauses into require calls. Each spec is quoted +;; individually and passed as data; non-list clauses (docstring, attr-map, +;; :gen-class, …) are ignored. So ns compiles to a plain (do …) of invokes. +;; MUST live in this first tier: the self-hosted analyzer build (triggered while +;; 10-seq loads) processes jolt.analyzer's own (ns …) form, so ns has to exist by +;; then. Its body resolves fn/map/reduce/cond at EXPANSION time, by which point all +;; of 00-syntax has loaded, so using them here is fine. +(defmacro ns [nm & clauses] + (let [calls (reduce + (fn [acc clause] + (if (seq? clause) + (let [head (first clause) args (rest clause)] + (cond + (= head :require) (conj acc `(require ~@(map (fn [s] `(quote ~s)) args))) + (= head :use) (conj acc `(use ~@(map (fn [s] `(quote ~s)) args))) + (= head :import) (conj acc `(import ~@(map (fn [s] `(quote ~s)) args))) + (= head :refer-clojure) + (conj acc `(refer-clojure ~@(map (fn [s] `(quote ~s)) args))) + :else acc)) + acc)) + [] clauses)] + `(do (in-ns (quote ~nm)) ~@calls))) + ;; Threading: a list form threads x in as the first (->) or last (->>) arg; a bare ;; symbol becomes (form x). Recursive; the expand-once cache makes that free. (defmacro -> [x & forms] diff --git a/src/jolt/compiler.janet b/src/jolt/compiler.janet index 7dac091..eba953e 100644 --- a/src/jolt/compiler.janet +++ b/src/jolt/compiler.janet @@ -159,7 +159,7 @@ "defmethod" "prefer-method" "remove-method" "remove-all-methods" "get-method" "methods" "satisfies?" "instance?" "set!" "var" "var-get" - "var-set" "var?" "ns" "create-ns" "remove-ns" + "var-set" "var?" "ns" "in-ns" "require" "create-ns" "remove-ns" "find-ns" "all-ns" "the-ns" "find-var" "intern" "resolve" "ns-resolve" "ns-aliases" "ns-imports" "ns-interns" "alter-var-root" "alter-meta!" "reset-meta!" "locking" "new" diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 1c9f3d3..a9b0d8c 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -772,6 +772,50 @@ (ctx-set-current-ns ctx ns-name) nil) +(defn use-impl + "(use '[ns ...] ...) — refer ALL public vars of each used ns into the CURRENT ns. + A fn; quoted specs arrive evaluated. Each spec is a ns symbol or a [ns & opts] + vector (a pvec/tuple, not a Janet array — coerce, then take the head as the ns)." + [ctx & specs] + (def target-ns (ctx-find-ns ctx (ctx-current-ns ctx))) + (each s specs + (let [spec (if (pvec? s) (pv->array s) s) + ns-sym (if (indexed? spec) (in spec 0) spec) + src-name (sym-name-str ns-sym)] + (maybe-require-ns ctx src-name) + (let [source-ns (ctx-find-ns ctx src-name)] + (loop [[sym v] :pairs (source-ns :mappings)] + (let [nv (ns-intern target-ns sym (var-get v))] + (when (get v :macro) (put nv :macro true))))))) + nil) + +(defn import-impl + "(import 'pkg.Class ...) — register the short class name as an alias of the fully + qualified name in the current ns. A fn; quoted class symbols arrive evaluated." + [ctx & class-specs] + (def ns (ctx-find-ns ctx (ctx-current-ns ctx))) + (each class-spec class-specs + (let [class-name (if (and (struct? class-spec) (= :symbol (class-spec :jolt/type))) + (class-spec :name) (string class-spec)) + last-dot (do (var idx -1) (var pos 0) + (while (< pos (length class-name)) + (when (= (class-name pos) 46) (set idx pos)) (++ pos)) + idx) + short-name (if (>= last-dot 0) (string/slice class-name (+ last-dot 1)) class-name)] + (ns-import ns short-name class-name))) + nil) + +(defn refer-clojure-impl + "(refer-clojure :exclude [a b]) — currently only :exclude is honored: unmap the + excluded names from the current ns. A fn; quoted args arrive evaluated." + [ctx & args] + (when (and (>= (length args) 2) (= (in args 0) :exclude)) + (let [ns (ctx-find-ns ctx (ctx-current-ns ctx)) + excl (in args 1)] + (each sym excl + (ns-unmap ns (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym)))))) + nil) + (defn install-stateful-fns! "Intern ctx-capturing closures for the stateful primitives into clojure.core, so both the interpreter and the compiler reach them as ordinary fns. Called by @@ -789,6 +833,9 @@ (fn [proto-name methods-map] (make-reified-impl ctx proto-name methods-map))) (ns-intern core "require" (fn [& specs] (require-impl ctx ;specs))) (ns-intern core "in-ns" (fn [sym] (in-ns-impl ctx sym))) + (ns-intern core "use" (fn [& specs] (use-impl ctx ;specs))) + (ns-intern core "import" (fn [& specs] (import-impl ctx ;specs))) + (ns-intern core "refer-clojure" (fn [& args] (refer-clojure-impl ctx ;args))) core) # Dispatch a special form by its string name. @@ -932,69 +979,9 @@ # A (re)defined macro invalidates any cached expansions. (table/clear macro-cache) (var-get v))) - "ns" (let [raw-name (in form 1) - name-sym (unwrap-meta-name raw-name) - ns-name (sym-name-str name-sym) - clauses (tuple/slice form 2)] - (ctx-set-current-ns ctx ns-name) - (ctx-find-ns ctx ns-name) - (var result nil) - (var i 0) - (let [clen (length clauses)] - (while (< i clen) - (let [clause (in clauses i) - head (if (and (array? clause) (> (length clause) 0)) (first clause) nil)] - (if (nil? head) - (do (set result clause) (++ i)) - (match head - :require (let [specs (tuple/slice clause 1) - slen (length specs)] - (var j 0) - (while (< j slen) - (let [s (in specs j)] - (when s (eval-require ctx s))) - (++ j)) - (set i (+ i 1))) - :use (let [specs (tuple/slice clause 1) - slen (length specs)] - (var j 0) - (while (< j slen) - (let [s (in specs j) - ns-sym (if (array? s) (in s 0) s) - ns-name (sym-name-str ns-sym) - source-ns (ctx-find-ns ctx ns-name) - target-ns (ctx-find-ns ctx ns-name)] - (loop [[sym v] :pairs (source-ns :mappings)] - (ns-intern target-ns sym (var-get v)))) - (++ j)) - (set i (+ i 1))) - :refer-clojure (let [spec (in clause 1)] - (when (and (array? spec) (= (first spec) :exclude)) - (let [ns (ctx-find-ns ctx ns-name)] - (each sym (tuple/slice spec 1) - (ns-unmap ns (if (struct? sym) (sym :name) sym))))) - (set i (+ i 1))) - :import (let [specs (tuple/slice clause 1) - slen (length specs)] - (var j 0) - (while (< j slen) - (let [class-spec (in specs j) - class-name (if (struct? class-spec) (class-spec :name) (string class-spec)) - last-dot (do - (var idx -1) - (var pos 0) - (while (< pos (length class-name)) - (if (= (class-name pos) 46) (set idx pos)) - (++ pos)) - idx) - short-name (if (>= last-dot 0) - (string/slice class-name (+ last-dot 1)) - class-name)] - (ns-import (ctx-find-ns ctx ns-name) short-name class-name)) - (++ j)) - (set i (+ i 1))) - (do (set result clause) (++ i))))))) - result) + # ns is now a macro (clojure.core, 30-macros) expanding to in-ns + require/use/ + # import/refer-clojure calls — all ctx-capturing fns — so it compiles. No + # special-form arm; an (ns ...) head falls through to the macro-expansion path. # require / in-ns are now ordinary clojure.core fns (install-stateful-fns!) — # no special-form arm; they compile + interpret as plain invokes. "all-ns" (all-ns ctx) diff --git a/src/jolt/host_iface.janet b/src/jolt/host_iface.janet index 802a720..13a0780 100644 --- a/src/jolt/host_iface.janet +++ b/src/jolt/host_iface.janet @@ -82,9 +82,10 @@ # ns-management forms dispatched by the interpreter (not core vars) "create-ns" "remove-ns" "find-ns" "all-ns" "the-ns" "resolve" "ns-resolve" "ns-aliases" "ns-imports" "ns-interns" - # require/in-ns are now clojure.core fns (compile as plain invokes). - "read-string" "macroexpand-1" "defonce" "ns" - "import" "use" "refer" "defrecord" + # ns/require/in-ns/use/import/refer-clojure are now clojure.core + # fns/macros (compile as plain invokes / expand to them). + "read-string" "macroexpand-1" "defonce" + "refer" "defrecord" # defprotocol/extend-type/extend-protocol/reify now expand to plain # def + protocol-dispatch/register-method/make-reified invokes. "gen-class" diff --git a/src/jolt/loader.janet b/src/jolt/loader.janet index d5e4103..c1223ec 100644 --- a/src/jolt/loader.janet +++ b/src/jolt/loader.janet @@ -13,7 +13,7 @@ # host-coupled set (Stage 2 jolt-eaa): forms move off this list as they gain a # compile path; syntax-quote already compiles via the analyzer's `handled` set. (defn- stateful-head? [head-name] - (or (= head-name "defmacro") (= head-name "ns") + (or (= head-name "defmacro") (= head-name "deftype") (= head-name "defmulti") (= head-name "defmethod") (= head-name "set!") (= head-name ".") (= head-name "new") diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index f400793..077913d 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -135,6 +135,8 @@ ### ---- HIGH: aliased namespace calls ---- ["require :as alias" "\"1,2,3\"" "(do (require (quote [clojure.string :as s])) (s/join \",\" [1 2 3]))"] + ["ns form + alias" "\"HI\"" "(do (ns my.a (:require [clojure.string :as s])) (s/upper-case \"hi\"))"] + ["ns :use refers" "42" "(do (ns src.u) (def helper 42) (ns dst.u (:use [src.u])) helper)"] ### ---- MED: missing core fns ---- ["peek vec" "3" "(peek [1 2 3])"] diff --git a/test/integration/fallback-zero-test.janet b/test/integration/fallback-zero-test.janet index 30529ce..3e1364f 100644 --- a/test/integration/fallback-zero-test.janet +++ b/test/integration/fallback-zero-test.janet @@ -47,6 +47,7 @@ # compiles over an INTERNED var; the built-in dynamic vars aren't interned yet, # so it's exercised end-to-end in the state spec instead.) "(require (quote [clojure.string :as s]))" "(in-ns (quote foo.bar))" + "(ns foo.bar (:require [clojure.string :as s]))" "(defprotocol P (m [x]))" "(extend-type Long P (m [x] x))" "(reify P (m [this] 1))" "(var map)"]) @@ -55,7 +56,7 @@ # (require/in-ns/protocols/binding now compile). The remaining frozen/uncompiled # set keeps the harness honest in the punt direction. (def must-punt - ["(ns foo.bar)" "(defmacro m [x] x)" "(deftype T [a])" + ["(defmacro m [x] x)" "(deftype T [a])" "(set! *warn-on-reflection* true)" "(letfn [(f [n] (g n)) (g [n] (f n))] (f 1))"]) (var fails @[]) diff --git a/test/spec/namespaces-spec.janet b/test/spec/namespaces-spec.janet index 951d09f..f69ebc9 100644 --- a/test/spec/namespaces-spec.janet +++ b/test/spec/namespaces-spec.janet @@ -16,6 +16,11 @@ (defspec "namespaces / ns operations" ["in-ns switches" "true" "(do (in-ns 'my.ns) (symbol? 'x))"] + # ns is a macro over in-ns/require/use/import (Stage 2 jolt-eaa): the form sets + # the current ns and processes its clauses. + ["ns form + alias" "\"HI\"" "(do (ns my.app (:require [clojure.string :as s])) (s/upper-case \"hi\"))"] + ["ns :use refers all" "9" "(do (ns src.lib) (def helper 9) (ns dst.app (:use [src.lib])) helper)"] + ["standalone use" "7" "(do (ns src.l2) (def k 7) (in-ns 'dst.a2) (use '[src.l2]) k)"] ["ns-name" "true" "(do (require (quote [clojure.string])) (= 'clojure.string (ns-name (find-ns 'clojure.string))))"] ["find-ns existing" "true" "(some? (find-ns 'clojure.core))"] ["find-ns missing" "nil" "(find-ns 'does.not.exist)"]