From e311018d5563ab947479de5d1076264af68bed98 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Wed, 10 Jun 2026 18:36:06 -0400 Subject: [PATCH] core: one alias store (jolt-ark); '.' classified as the special form it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/spec/coverage.md | 2 +- src/jolt/evaluator.janet | 32 ++++++++++++++++----------- src/jolt/types.janet | 15 +++++++++---- test/integration/namespace-test.janet | 4 ++-- test/spec/namespaces-spec.janet | 18 +++++++++++++++ tools/spec_coverage.py | 6 +++-- 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/spec/coverage.md b/docs/spec/coverage.md index 93cedaa..1158718 100644 --- a/docs/spec/coverage.md +++ b/docs/spec/coverage.md @@ -9,7 +9,7 @@ community examples). jolt interns 564 of them. |---|---|---| | implemented+tested | 564 | in jolt and exercised by spec/conformance | | 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 | | special-form | 15 | specified in §3, not a library var | | dynamic-var | 29 | classification needed: portable default vs host-dependent | diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index 9b6c19f..8f790d0 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -286,7 +286,7 @@ # 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)) 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))] (ns-find target-ns name)) (if (get bindings name) nil @@ -374,7 +374,7 @@ (maybe-require-ns ctx ns-name) (when alias (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 (let [source-ns (ctx-find-ns ctx ns-name) 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 (not (nil? ns)) (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)) v (and target-ns (ns-find target-ns name))] (if v (var-get v) @@ -1026,7 +1026,14 @@ # 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. (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-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 @@ -1038,8 +1045,9 @@ (def nm (if (struct? sym) (sym :name) (string sym))) (def nsp (if (struct? sym) (sym :ns) nil)) (if nsp - (let [alias-ns (get (ns :aliases) nsp)] - (when alias-ns (ns-find alias-ns nm))) + (let [target (or (ns-alias-lookup ns nsp) nsp) + target-ns (ctx-find-ns ctx target)] + (when target-ns (ns-find target-ns nm))) (ns-find ns nm)))) (ns-intern core "resolve" (fn [sym] @@ -1185,20 +1193,18 @@ (var nxt (expand-1 cur)) (while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur))) cur)) - # alias/ns-unalias: alias bookkeeping is currently split (require :as writes - # the string-keyed :imports table that resolution reads; :aliases is the - # introspection table ns-aliases reads) — write/remove BOTH until unified. + # alias bookkeeping is UNIFIED (jolt-ark): :aliases (alias-name string -> + # ns-name string) is the one store, read by resolution and ns-aliases; + # :imports holds class imports only. (ns-intern core "alias" (fn [alias-sym ns-sym] (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 (ctx-find-ns ctx (ns-sym :name))) + (ns-add-alias cur (alias-sym :name) (ns-sym :name)) nil)) (ns-intern core "ns-unalias" (fn [ns-d alias-sym] (def ns (ns-or-current ns-d)) - (put (ns :imports) (alias-sym :name) nil) - (put (ns :aliases) alias-sym nil) + (put (ns :aliases) (alias-sym :name) nil) nil)) # ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns). # Keys are symbol structs (value-hashed), matching Clojure's symbol keys. diff --git a/src/jolt/types.janet b/src/jolt/types.janet index 6594dcb..d394ae4 100644 --- a/src/jolt/types.janet +++ b/src/jolt/types.janet @@ -279,7 +279,7 @@ (or (ns-find ns sym) (let [qualified? (sym? sym)] (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))] (when alias-ns (ns-find alias-ns (sym :name)))))))) @@ -295,9 +295,16 @@ (get (ns :imports) class-name)) (defn ns-add-alias - "Add an alias from alias-sym to target-ns." - [ns alias-sym target-ns] - (put (ns :aliases) alias-sym target-ns)) + "Add an alias: alias-name (string) -> target ns NAME (string). The ONE alias + store (jolt-ark): resolution and ns-aliases both read it; :imports is class + 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 diff --git a/test/integration/namespace-test.janet b/test/integration/namespace-test.janet index e3d3417..b11fa3d 100644 --- a/test/integration/namespace-test.janet +++ b/test/integration/namespace-test.janet @@ -47,7 +47,7 @@ (assert (= "my.app" (ctx-current-ns ctx)) "ns with require sets current namespace") # Alias should be registered (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"))) (print " passed") @@ -55,7 +55,7 @@ (let [ctx (fresh-ctx)] (eval-form ctx @{} (parse-string "(require '[other.lib :as o])")) (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"))) (print " passed") diff --git a/test/spec/namespaces-spec.janet b/test/spec/namespaces-spec.janet index 4ee3342..9b26b55 100644 --- a/test/spec/namespaces-spec.janet +++ b/test/spec/namespaces-spec.janet @@ -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\"))"] ["*ns* restored after throw" "\"user\"" "(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)))"]) diff --git a/tools/spec_coverage.py b/tools/spec_coverage.py index 6fab343..dbc5d6c 100644 --- a/tools/spec_coverage.py +++ b/tools/spec_coverage.py @@ -54,7 +54,9 @@ for f in glob.glob('test/spec/*.janet') + ['test/integration/conformance-test.ja # --- classification --------------------------------------------------------- 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', 'agent-error','agent-errors','clear-agent-errors','error-handler', '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-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 | | special-form | {counts['special-form']} | specified in §3, not a library var | | dynamic-var | {counts['dynamic-var']} | classification needed: portable default vs host-dependent |