core: one alias store (jolt-ark); '.' classified as the special form it is

require-:as wrote the string-keyed :imports table (which resolution reads)
while ns-aliases read the symbol-keyed :aliases table (which nothing wrote)
— so (ns-aliases) was always empty and the alias fn had to write both as a
bridge. :aliases (alias-name string -> ns-name string) is THE store now:
require :as and the alias fn write it, both resolution paths read it first
(falling back to :imports for class imports, which is all that table holds
now), ns-unalias removes one entry, and ns-aliases presents Clojure's
{alias-symbol -> namespace object} shape built from it. ns-resolve's
qualified path goes through the same lookup.

Also: the coverage dashboard's last 'resolvable-not-interned' entry was '.'
— which (resolve '.) returns nil for on the JVM too; the tool now classifies
it as the special form it is, and that category reads ZERO.

7 new unified-alias spec rows (require/alias/ns-unalias round-trips through
both the resolution and introspection views); the white-box namespace test
tracks the accessor rename. Gate exit 0.
This commit is contained in:
Yogthos 2026-06-10 18:36:06 -04:00
parent 7c2d556dc5
commit e311018d55
6 changed files with 55 additions and 22 deletions

View file

@ -9,7 +9,7 @@ community examples). jolt interns 564 of them.
|---|---|---| |---|---|---|
| implemented+tested | 564 | in jolt and exercised by spec/conformance | | implemented+tested | 564 | in jolt and exercised by spec/conformance |
| implemented-untested | 0 | in jolt, no direct test — spec entries will add them | | implemented-untested | 0 | in jolt, no direct test — spec entries will add them |
| resolvable-not-interned | 1 | works in code but invisible to ns introspection (conformance finding) | | resolvable-not-interned | 0 | works in code but invisible to ns introspection (conformance finding) |
| missing-portable | 0 | portable semantics, jolt lacks it — implementation gap | | missing-portable | 0 | portable semantics, jolt lacks it — implementation gap |
| special-form | 15 | specified in §3, not a library var | | special-form | 15 | specified in §3, not a library var |
| dynamic-var | 29 | classification needed: portable default vs host-dependent | | dynamic-var | 29 | classification needed: portable default vs host-dependent |

View file

@ -286,7 +286,7 @@
# aliased ref like g/foo wouldn't resolve mid-compile. Same ns h-current-ns uses. # aliased ref like g/foo wouldn't resolve mid-compile. Same ns h-current-ns uses.
(let [cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx)) (let [cur-name (or (get (ctx :env) :compile-ns) (ctx-current-ns ctx))
current-ns (ctx-find-ns ctx cur-name) current-ns (ctx-find-ns ctx cur-name)
aliased-ns (ns-import-lookup current-ns ns) aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns ns))] target-ns (ctx-find-ns ctx (or aliased-ns ns))]
(ns-find target-ns name)) (ns-find target-ns name))
(if (get bindings name) nil (if (get bindings name) nil
@ -374,7 +374,7 @@
(maybe-require-ns ctx ns-name) (maybe-require-ns ctx ns-name)
(when alias (when alias
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))] (let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
(ns-import current-ns alias ns-name))) (ns-add-alias current-ns alias ns-name)))
(when refer-syms (when refer-syms
(let [source-ns (ctx-find-ns ctx ns-name) (let [source-ns (ctx-find-ns ctx ns-name)
target-ns (ctx-find-ns ctx (ctx-current-ns ctx))] target-ns (ctx-find-ns ctx (ctx-current-ns ctx))]
@ -436,7 +436,7 @@
(if (nil? v) (error (string "Unsupported Thread member: Thread/" name)) v)) (if (nil? v) (error (string "Unsupported Thread member: Thread/" name)) v))
(if (not (nil? ns)) (if (not (nil? ns))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx)) (let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))
aliased-ns (ns-import-lookup current-ns ns) aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns))
target-ns (ctx-find-ns ctx (or aliased-ns ns)) target-ns (ctx-find-ns ctx (or aliased-ns ns))
v (and target-ns (ns-find target-ns name))] v (and target-ns (ns-find target-ns name))]
(if v (var-get v) (if v (var-get v)
@ -1026,7 +1026,14 @@
# interns/imports return a jolt MAP (struct), not the live host table — so # interns/imports return a jolt MAP (struct), not the live host table — so
# count/seq/keys work on them, and callers can't mutate the ns through them. # count/seq/keys work on them, and callers can't mutate the ns through them.
(ns-intern core "ns-interns" (fn [&opt x] (table/to-struct ((ns-or-current x) :mappings)))) (ns-intern core "ns-interns" (fn [&opt x] (table/to-struct ((ns-or-current x) :mappings))))
(ns-intern core "ns-aliases" (fn [&opt x] ((ns-or-current x) :aliases))) # {alias-symbol -> namespace object}, Clojure's shape, from the string store.
(ns-intern core "ns-aliases"
(fn [&opt x]
(def ns (ns-or-current x))
(def out @{})
(eachp [a target] (ns :aliases)
(put out {:jolt/type :symbol :ns nil :name a} (ctx-find-ns ctx target)))
(table/to-struct out)))
(ns-intern core "ns-imports" (fn [&opt x] (table/to-struct ((ns-or-current x) :imports)))) (ns-intern core "ns-imports" (fn [&opt x] (table/to-struct ((ns-or-current x) :imports))))
# (ns-resolve ns sym) -> the var or nil. Unqualified syms look in ns's own # (ns-resolve ns sym) -> the var or nil. Unqualified syms look in ns's own
# mappings; ns-qualified syms resolve through ns's aliases. (types/ns-resolve # mappings; ns-qualified syms resolve through ns's aliases. (types/ns-resolve
@ -1038,8 +1045,9 @@
(def nm (if (struct? sym) (sym :name) (string sym))) (def nm (if (struct? sym) (sym :name) (string sym)))
(def nsp (if (struct? sym) (sym :ns) nil)) (def nsp (if (struct? sym) (sym :ns) nil))
(if nsp (if nsp
(let [alias-ns (get (ns :aliases) nsp)] (let [target (or (ns-alias-lookup ns nsp) nsp)
(when alias-ns (ns-find alias-ns nm))) target-ns (ctx-find-ns ctx target)]
(when target-ns (ns-find target-ns nm)))
(ns-find ns nm)))) (ns-find ns nm))))
(ns-intern core "resolve" (ns-intern core "resolve"
(fn [sym] (fn [sym]
@ -1185,20 +1193,18 @@
(var nxt (expand-1 cur)) (var nxt (expand-1 cur))
(while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur))) (while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur)))
cur)) cur))
# alias/ns-unalias: alias bookkeeping is currently split (require :as writes # alias bookkeeping is UNIFIED (jolt-ark): :aliases (alias-name string ->
# the string-keyed :imports table that resolution reads; :aliases is the # ns-name string) is the one store, read by resolution and ns-aliases;
# introspection table ns-aliases reads) — write/remove BOTH until unified. # :imports holds class imports only.
(ns-intern core "alias" (ns-intern core "alias"
(fn [alias-sym ns-sym] (fn [alias-sym ns-sym]
(def cur (ctx-find-ns ctx (ctx-current-ns ctx))) (def cur (ctx-find-ns ctx (ctx-current-ns ctx)))
(ns-import cur (alias-sym :name) (ns-sym :name)) (ns-add-alias cur (alias-sym :name) (ns-sym :name))
(ns-add-alias cur alias-sym (ctx-find-ns ctx (ns-sym :name)))
nil)) nil))
(ns-intern core "ns-unalias" (ns-intern core "ns-unalias"
(fn [ns-d alias-sym] (fn [ns-d alias-sym]
(def ns (ns-or-current ns-d)) (def ns (ns-or-current ns-d))
(put (ns :imports) (alias-sym :name) nil) (put (ns :aliases) (alias-sym :name) nil)
(put (ns :aliases) alias-sym nil)
nil)) nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns). # ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys. # Keys are symbol structs (value-hashed), matching Clojure's symbol keys.

View file

@ -279,7 +279,7 @@
(or (ns-find ns sym) (or (ns-find ns sym)
(let [qualified? (sym? sym)] (let [qualified? (sym? sym)]
(when qualified? (when qualified?
# qualified symbol: look up via alias # qualified symbol: look up via alias (string-keyed store)
(let [alias-ns (get (ns :aliases) (sym :ns))] (let [alias-ns (get (ns :aliases) (sym :ns))]
(when alias-ns (when alias-ns
(ns-find alias-ns (sym :name)))))))) (ns-find alias-ns (sym :name))))))))
@ -295,9 +295,16 @@
(get (ns :imports) class-name)) (get (ns :imports) class-name))
(defn ns-add-alias (defn ns-add-alias
"Add an alias from alias-sym to target-ns." "Add an alias: alias-name (string) -> target ns NAME (string). The ONE alias
[ns alias-sym target-ns] store (jolt-ark): resolution and ns-aliases both read it; :imports is class
(put (ns :aliases) alias-sym target-ns)) imports only."
[ns alias-name target-ns-name]
(put (ns :aliases) alias-name target-ns-name))
(defn ns-alias-lookup
"The target ns NAME for alias-name, or nil."
[ns alias-name]
(get (ns :aliases) alias-name))
# ============================================================ # ============================================================
# Context # Context

View file

@ -47,7 +47,7 @@
(assert (= "my.app" (ctx-current-ns ctx)) "ns with require sets current namespace") (assert (= "my.app" (ctx-current-ns ctx)) "ns with require sets current namespace")
# Alias should be registered # Alias should be registered
(let [ns (ctx-find-ns ctx "my.app") (let [ns (ctx-find-ns ctx "my.app")
aliased (ns-import-lookup ns "o")] aliased (ns-alias-lookup ns "o")]
(assert (= "other.lib" aliased) "alias o -> other.lib registered"))) (assert (= "other.lib" aliased) "alias o -> other.lib registered")))
(print " passed") (print " passed")
@ -55,7 +55,7 @@
(let [ctx (fresh-ctx)] (let [ctx (fresh-ctx)]
(eval-form ctx @{} (parse-string "(require '[other.lib :as o])")) (eval-form ctx @{} (parse-string "(require '[other.lib :as o])"))
(let [ns (ctx-find-ns ctx "user") (let [ns (ctx-find-ns ctx "user")
aliased (ns-import-lookup ns "o")] aliased (ns-alias-lookup ns "o")]
(assert (= "other.lib" aliased) "standalone require registers alias"))) (assert (= "other.lib" aliased) "standalone require registers alias")))
(print " passed") (print " passed")

View file

@ -53,3 +53,21 @@
"(do (require (quote [clojure.string :as s9])) (try (s9/join nil nil nil) (catch Exception e nil)) (s9/upper-case \"a\"))"] "(do (require (quote [clojure.string :as s9])) (try (s9/join nil nil nil) (catch Exception e nil)) (s9/upper-case \"a\"))"]
["*ns* restored after throw" "\"user\"" ["*ns* restored after throw" "\"user\""
"(do (require (quote [clojure.walk :as w9])) (try (w9/postwalk nil nil nil) (catch Exception e nil)) (str *ns*))"]) "(do (require (quote [clojure.walk :as w9])) (try (w9/postwalk nil nil nil) (catch Exception e nil)) (str *ns*))"])
# Alias bookkeeping is unified (jolt-ark): one string-keyed :aliases store,
# read by resolution AND ns-aliases (which presents Clojure's
# {alias-symbol -> namespace} shape); :imports holds class imports only.
(defspec "namespaces / unified alias store"
["require :as registers the alias" "1"
"(do (require (quote [clojure.string :as st1])) (count (filter (fn [[a n]] (= (str a) \"st1\")) (ns-aliases))))"]
["aliased call resolves" "\"A\""
"(do (require (quote [clojure.string :as st2])) (st2/upper-case \"a\"))"]
["alias fn registers + resolves" "\"B\""
"(do (require (quote [clojure.string])) (alias (quote st3) (quote clojure.string)) (st3/upper-case \"b\"))"]
["alias fn visible to ns-aliases" "true"
"(do (require (quote [clojure.string])) (alias (quote st4) (quote clojure.string)) (pos? (count (filter (fn [[a n]] (= (str a) \"st4\")) (ns-aliases)))))"]
["ns-unalias removes both views" "[0 false]"
"(do (require (quote [clojure.string :as st5])) (ns-unalias (quote user) (quote st5)) [(count (filter (fn [[a n]] (= (str a) \"st5\")) (ns-aliases))) (boolean (resolve (quote st5/upper-case)))])"]
["ns-resolve through alias" "true"
"(do (require (quote [clojure.string :as st6])) (var? (ns-resolve (quote user) (quote st6/upper-case))))"]
["empty ns-aliases is a map" "true" "(map? (ns-aliases (quote clojure.core)))"])

View file

@ -54,7 +54,9 @@ for f in glob.glob('test/spec/*.janet') + ['test/integration/conformance-test.ja
# --- classification --------------------------------------------------------- # --- classification ---------------------------------------------------------
SPECIAL = {'catch','finally','do','def','defmacro','fn','if','let','loop','quote', SPECIAL = {'catch','finally','do','def','defmacro','fn','if','let','loop','quote',
'recur','throw','try','var','new','set!','monitor-enter','monitor-exit'} 'recur','throw','try','var','new','set!','monitor-enter','monitor-exit',
# '.' is the interop special form — (resolve '.) is nil on the JVM too
'.'}
AGENTS = {'agent','send','send-off','send-via','await','await-for','await1', AGENTS = {'agent','send','send-off','send-via','await','await-for','await1',
'agent-error','agent-errors','clear-agent-errors','error-handler', 'agent-error','agent-errors','clear-agent-errors','error-handler',
'error-mode','set-agent-send-executor!','set-agent-send-off-executor!', 'error-mode','set-agent-send-executor!','set-agent-send-off-executor!',
@ -105,7 +107,7 @@ community examples). jolt interns {len(jolt & set(core))} of them.
|---|---|---| |---|---|---|
| implemented+tested | {counts['implemented+tested']} | in jolt and exercised by spec/conformance | | implemented+tested | {counts['implemented+tested']} | in jolt and exercised by spec/conformance |
| implemented-untested | {counts['implemented-untested']} | in jolt, no direct test spec entries will add them | | implemented-untested | {counts['implemented-untested']} | in jolt, no direct test spec entries will add them |
| resolvable-not-interned | {len((resolvable - interned) & set(core))} | works in code but invisible to ns introspection (conformance finding) | | resolvable-not-interned | {len((resolvable - interned) & set(core) - SPECIAL)} | works in code but invisible to ns introspection (conformance finding) |
| missing-portable | {counts['missing-portable']} | portable semantics, jolt lacks it implementation gap | | missing-portable | {counts['missing-portable']} | portable semantics, jolt lacks it implementation gap |
| special-form | {counts['special-form']} | specified in §3, not a library var | | special-form | {counts['special-form']} | specified in §3, not a library var |
| dynamic-var | {counts['dynamic-var']} | classification needed: portable default vs host-dependent | | dynamic-var | {counts['dynamic-var']} | classification needed: portable default vs host-dependent |