core: java shims for yogthos/config + three conformance fixes

yogthos/config now loads and runs end to end: config.edn/.lein-env
deep merge, env vars keywordized and type-converted, PushbackReader
over io/reader, reload-env via alter-var-root. New shims:
java.io.PushbackReader (read/unread), Long/parseLong, BigInteger.,
Boolean/parseBoolean, System/getProperties; clojure.edn/read now
drains an actual reader (it used to read one LINE from a raw janet
file handle, so multi-line config files and shim readers both broke).

Three real bugs shaken out, each with regression specs:

- An empty rest arg bound () instead of nil. ((fn [& r] (if r 1 2)))
  returned 1; the truthy () sent config's (or (.exists f) required)
  down the wrong branch. Fixed in the interpreter and the compiled-fn
  emission. Internal apply boundaries (protocol dispatch, core-apply)
  now accept a nil seq like Clojure's (apply f x nil).

- seq/map over a raw janet table (System/getenv, os/environ) yielded
  nothing, so config's read-system-env came back empty. Raw host
  tables now seq as kv entries like any map, in core-seq,
  realize-for-iteration, and coll->cells. The old spec row hid this
  behind a vacuous (every? pred empty) — replaced with one that
  asserts non-emptiness.

- edn/read single-line limitation, as above.

test/integration/config-lib-test.janet runs the real library from
~/src/config (skips when absent).
This commit is contained in:
Yogthos 2026-06-11 00:11:04 -04:00
parent 1673a0f024
commit d161e16df6
8 changed files with 215 additions and 12 deletions

View file

@ -181,3 +181,16 @@
"((fn f [acc & xs] (if (seq xs) (recur acc (rest xs)) :empty)) 0 1)"]
["fixed-arity recur untouched" "10"
"((fn f [n acc] (if (pos? n) (recur (dec n) (+ acc 2)) acc)) 5 0)"])
# An EMPTY rest arg binds nil, never () — Clojure semantics. (f) with [& r]
# giving a truthy () sent yogthos/config's (or (.exists f) required) down the
# wrong branch and broke its load.
(defspec "functions / empty rest arg is nil"
["no args" ":nil" "((fn [& r] (if r :truthy :nil)))"]
["after fixed" ":nil" "((fn [a & r] (if r :truthy :nil)) 1)"]
["via apply" ":nil" "(apply (fn [& r] (if r :truthy :nil)) [])"]
["defn no args" ":nil" "(do (defn er-f [& r] (if r :truthy :nil)) (er-f))"]
["non-empty unchanged" "'(1 2)" "((fn [& r] r) 1 2)"]
["one extra" "'(2)" "((fn [a & r] r) 1 2)"]
["rest destructure with no args" ":nil"
"((fn [& [a]] (if a :truthy :nil)))"])

View file

@ -109,3 +109,34 @@
"(with-open [r (StringReader. \"a\")] (.read r))"]
["vector :import shares deftype ctor" "\"hi!\""
"(do (ns spec.nodea) (defprotocol SpecP (spec-pm [this])) (deftype SpecTN [t] SpecP (spec-pm [this] (str t \"!\"))) (ns spec.nodeb (:import [spec.nodea SpecTN])) (.spec-pm (SpecTN. \"hi\")))"])
# Shims for yogthos/config: PushbackReader, numeric/boolean parse statics,
# System/getenv + getProperties as iterable maps, edn/read from a reader.
(defspec "interop / PushbackReader & parse statics"
["PushbackReader read" "[97 98]"
"(let [r (java.io.PushbackReader. (java.io.StringReader. \"ab\"))] [(.read r) (.read r)])"]
["unread pushes back" "[97 97 98]"
"(let [r (PushbackReader. (StringReader. \"ab\")) a (.read r)] (.unread r a) [a (.read r) (.read r)])"]
["unread accepts a char" "[120 97]"
"(let [r (PushbackReader. (StringReader. \"a\"))] (.unread r \\x) [(.read r) (.read r)])"]
["edn/read from reader" "5432"
"(do (require (quote clojure.edn)) (clojure.edn/read (java.io.PushbackReader. (java.io.StringReader. \"{:db {:port 5432}}\\nrest\"))) (get-in (clojure.edn/read-string \"{:db {:port 5432}}\") [:db :port]))"]
["edn/read multi-line" "true"
"(do (require (quote clojure.edn)) (= {:a 1 :b 2} (clojure.edn/read (PushbackReader. (StringReader. \"{:a 1\\n :b 2}\")))))"]
["Long/parseLong" "42" "(Long/parseLong \"42\")"]
["parseLong rejects non-numeric" :throws "(Long/parseLong \"4x\")"]
["BigInteger." "123" "(BigInteger. \"123\")"]
["Boolean/parseBoolean" "[true false false]"
"[(Boolean/parseBoolean \"true\") (Boolean/parseBoolean \"false\") (Boolean/parseBoolean \"yes\")]"]
["System/getenv is a map" "true"
"(string? (get (System/getenv) \"HOME\"))"]
# NOT every? alone — it held vacuously while seq over a raw host table
# yielded nothing, hiding that read-system-env came back empty
["getenv entries destructure (non-empty)" "true"
"(let [es (map (fn [[k v]] [k v]) (System/getenv))] (and (pos? (count es)) (every? vector? es)))"]
["seq over a raw host table" "true"
"(pos? (count (seq (System/getenv))))"]
["into {} from host table" "true"
"(string? (get (into {} (map (fn [[k v]] [k v]) (System/getenv))) \"HOME\"))"]
["System/getProperties" "true"
"(string? (get (System/getProperties) \"os.name\"))"])