Stage2 task2 tier4b (#15)

* 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 <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-10 04:34:53 +08:00 committed by GitHub
parent e8c19c351f
commit 63d92cd122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 90 additions and 69 deletions

View file

@ -36,6 +36,31 @@
nil nil
`(if ~(first clauses) ~(nth clauses 1) (cond ~@(drop 2 clauses))))) `(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 ;; 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. ;; symbol becomes (form x). Recursive; the expand-once cache makes that free.
(defmacro -> [x & forms] (defmacro -> [x & forms]

View file

@ -159,7 +159,7 @@
"defmethod" "prefer-method" "remove-method" "remove-all-methods" "defmethod" "prefer-method" "remove-method" "remove-all-methods"
"get-method" "methods" "get-method" "methods"
"satisfies?" "instance?" "set!" "var" "var-get" "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" "find-ns" "all-ns" "the-ns" "find-var" "intern" "resolve"
"ns-resolve" "ns-aliases" "ns-imports" "ns-interns" "ns-resolve" "ns-aliases" "ns-imports" "ns-interns"
"alter-var-root" "alter-meta!" "reset-meta!" "locking" "new" "alter-var-root" "alter-meta!" "reset-meta!" "locking" "new"

View file

@ -772,6 +772,50 @@
(ctx-set-current-ns ctx ns-name) (ctx-set-current-ns ctx ns-name)
nil) 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! (defn install-stateful-fns!
"Intern ctx-capturing closures for the stateful primitives into clojure.core, so "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 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))) (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 "require" (fn [& specs] (require-impl ctx ;specs)))
(ns-intern core "in-ns" (fn [sym] (in-ns-impl ctx sym))) (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) core)
# Dispatch a special form by its string name. # Dispatch a special form by its string name.
@ -932,69 +979,9 @@
# A (re)defined macro invalidates any cached expansions. # A (re)defined macro invalidates any cached expansions.
(table/clear macro-cache) (table/clear macro-cache)
(var-get v))) (var-get v)))
"ns" (let [raw-name (in form 1) # ns is now a macro (clojure.core, 30-macros) expanding to in-ns + require/use/
name-sym (unwrap-meta-name raw-name) # import/refer-clojure calls — all ctx-capturing fns — so it compiles. No
ns-name (sym-name-str name-sym) # special-form arm; an (ns ...) head falls through to the macro-expansion path.
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)
# require / in-ns are now ordinary clojure.core fns (install-stateful-fns!) — # require / in-ns are now ordinary clojure.core fns (install-stateful-fns!) —
# no special-form arm; they compile + interpret as plain invokes. # no special-form arm; they compile + interpret as plain invokes.
"all-ns" (all-ns ctx) "all-ns" (all-ns ctx)

View file

@ -82,9 +82,10 @@
# ns-management forms dispatched by the interpreter (not core vars) # ns-management forms dispatched by the interpreter (not core vars)
"create-ns" "remove-ns" "find-ns" "all-ns" "the-ns" "resolve" "create-ns" "remove-ns" "find-ns" "all-ns" "the-ns" "resolve"
"ns-resolve" "ns-aliases" "ns-imports" "ns-interns" "ns-resolve" "ns-aliases" "ns-imports" "ns-interns"
# require/in-ns are now clojure.core fns (compile as plain invokes). # ns/require/in-ns/use/import/refer-clojure are now clojure.core
"read-string" "macroexpand-1" "defonce" "ns" # fns/macros (compile as plain invokes / expand to them).
"import" "use" "refer" "defrecord" "read-string" "macroexpand-1" "defonce"
"refer" "defrecord"
# defprotocol/extend-type/extend-protocol/reify now expand to plain # defprotocol/extend-type/extend-protocol/reify now expand to plain
# def + protocol-dispatch/register-method/make-reified invokes. # def + protocol-dispatch/register-method/make-reified invokes.
"gen-class" "gen-class"

View file

@ -13,7 +13,7 @@
# host-coupled set (Stage 2 jolt-eaa): forms move off this list as they gain a # 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. # compile path; syntax-quote already compiles via the analyzer's `handled` set.
(defn- stateful-head? [head-name] (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 "deftype") (= head-name "defmulti") (= head-name "defmethod")
(= head-name "set!") (= head-name "set!")
(= head-name ".") (= head-name "new") (= head-name ".") (= head-name "new")

View file

@ -135,6 +135,8 @@
### ---- HIGH: aliased namespace calls ---- ### ---- HIGH: aliased namespace calls ----
["require :as alias" "\"1,2,3\"" "(do (require (quote [clojure.string :as s])) (s/join \",\" [1 2 3]))"] ["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 ---- ### ---- MED: missing core fns ----
["peek vec" "3" "(peek [1 2 3])"] ["peek vec" "3" "(peek [1 2 3])"]

View file

@ -47,6 +47,7 @@
# compiles over an INTERNED var; the built-in dynamic vars aren't interned yet, # 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.) # so it's exercised end-to-end in the state spec instead.)
"(require (quote [clojure.string :as s]))" "(in-ns (quote foo.bar))" "(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))" "(defprotocol P (m [x]))" "(extend-type Long P (m [x] x))"
"(reify P (m [this] 1))" "(var map)"]) "(reify P (m [this] 1))" "(var map)"])
@ -55,7 +56,7 @@
# (require/in-ns/protocols/binding now compile). The remaining frozen/uncompiled # (require/in-ns/protocols/binding now compile). The remaining frozen/uncompiled
# set keeps the harness honest in the punt direction. # set keeps the harness honest in the punt direction.
(def must-punt (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))"]) "(set! *warn-on-reflection* true)" "(letfn [(f [n] (g n)) (g [n] (f n))] (f 1))"])
(var fails @[]) (var fails @[])

View file

@ -16,6 +16,11 @@
(defspec "namespaces / ns operations" (defspec "namespaces / ns operations"
["in-ns switches" "true" "(do (in-ns 'my.ns) (symbol? 'x))"] ["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))))"] ["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 existing" "true" "(some? (find-ns 'clojure.core))"]
["find-ns missing" "nil" "(find-ns 'does.not.exist)"] ["find-ns missing" "nil" "(find-ns 'does.not.exist)"]