Refactor phase 1: consolidate host interop into one module (#107)
Cheap-version consolidation of the host-interop sprawl (jolt-jx5l). The JVM class/method shims were scattered across javatime.janet, evaluator.janet, api.janet and core.janet; the recent hiccup/markdown/malli fixes landed ad hoc. - Rename javatime.janet -> host_interop.janet. The name described ~20% of its contents (java.io/util/net/sql/lang all lived there); it is now the one home for host shims, and greppable. - Move the four hardcoded static tables (Math/Thread/System/Long) out of the evaluator and register them through the generic class-statics registry. The special-case dispatch in resolve-sym is deleted — one mechanism, not two. - Move the collection-interop wiring (set-coll-realizer!, set-coll-interop!, malli's LazilyPersistentVector/PersistentArrayMap statics) from api.janet into host_interop's install-collections!. api's 40 lines of wiring become a single (import ./host_interop). No behavior change. Full per-package src/jolt/interop/ split is the follow-up; string/number/object method tables and core's File/JDBC ctors stay put for now (they're coupled to the dot-dispatch / collection layer). Full gate green. Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
parent
910c4b6c99
commit
62fe11fa0d
4 changed files with 153 additions and 148 deletions
|
|
@ -12,48 +12,9 @@
|
|||
(import ./backend :as backend)
|
||||
(import ./stdlib_embed :as stdlib-embed)
|
||||
(import ./host_iface :as host)
|
||||
(import ./javatime) # java.time shims register into the evaluator at load
|
||||
(import ./phm :as mphm)
|
||||
|
||||
# Wire core's collection realizer into the evaluator's (.iterator coll) shim —
|
||||
# late-bound here because the evaluator loads before core. Makes Java-Iterator-
|
||||
# style loops (e.g. hiccup's iterate!) work over any jolt collection.
|
||||
(set-coll-realizer! realize-for-iteration)
|
||||
|
||||
# clj-targeted libraries call JVM collection-builder statics in their :clj
|
||||
# branches; malli's entry parser uses these. Map them to jolt's own builders.
|
||||
# createOwning takes ownership of an object array -> a vector. createWithCheck
|
||||
# builds a map and throws on duplicate keys (malli catches that to report them);
|
||||
# detect a dup by the built map being smaller than the kv array implies.
|
||||
(register-class-statics! "LazilyPersistentVector"
|
||||
@{"createOwning" (fn [arr] (make-vec arr))})
|
||||
(register-class-statics! "PersistentArrayMap"
|
||||
@{"createWithCheck"
|
||||
(fn [arr]
|
||||
(def m (mphm/make-phm arr))
|
||||
(if (= (* 2 (mphm/phm-count m)) (length arr)) m
|
||||
(error "PersistentArrayMap: duplicate key")))})
|
||||
|
||||
# Java collection-interop methods on jolt persistent collections — clj-targeted
|
||||
# libraries (malli) call .nth/.count/.valAt on vectors/maps in their :clj
|
||||
# branches. Route to the clojure.core equivalent; :jolt/ci-none means "not mine".
|
||||
(set-coll-interop!
|
||||
(fn [target name args]
|
||||
(if-not (or (pvec? target) (mphm/phm? target) (plist? target) (mphm/lazy-seq? target)
|
||||
(and (table? target) (= :jolt/set (get target :jolt/type)))
|
||||
(shape-rec? target) # map-as-tuple record
|
||||
(and (struct? target) (nil? (get target :jolt/type)))) # plain map literal
|
||||
:jolt/ci-none # only jolt persistent collections / maps — never records/reifies
|
||||
(cond
|
||||
(= name "nth") (if (>= (length args) 2) (core-nth target (in args 0) (in args 1))
|
||||
(core-nth target (in args 0)))
|
||||
(= name "count") (core-count target)
|
||||
(or (= name "valAt") (= name "get"))
|
||||
(if (>= (length args) 2) (core-get target (in args 0) (in args 1))
|
||||
(core-get target (in args 0)))
|
||||
(= name "seq") (core-seq target)
|
||||
(= name "containsKey") (core-contains? target (in args 0))
|
||||
:jolt/ci-none))))
|
||||
# Host interop: JVM class/method shims + collection interop (.iterator/.nth/...)
|
||||
# register into the evaluator at module load. See src/jolt/host_interop.janet.
|
||||
(import ./host_interop)
|
||||
|
||||
# A defmacro expander compiles to a native fn (built as (fn args body...) and run
|
||||
# through the self-hosted pipeline) so macro expansion is COMPILED code, zero runtime
|
||||
|
|
|
|||
|
|
@ -1778,7 +1778,7 @@
|
|||
# callbacks: :exec runs one SQL string in the transaction (so the janet-side
|
||||
# Statement.executeBatch can run SQL without a janet->clj call), :close closes
|
||||
# the underlying conn, :product is the JDBC database product name. instance?
|
||||
# Connection (evaluator) and the :jolt/jdbc-conn tagged methods (javatime) key
|
||||
# Connection (evaluator) and the :jolt/jdbc-conn tagged methods (host_interop) key
|
||||
# off this shape.
|
||||
(defn core-jdbc-wrap-conn [raw exec closef product]
|
||||
@{:jolt/type :jolt/jdbc-conn :raw raw :exec exec :close closef
|
||||
|
|
@ -1794,7 +1794,7 @@
|
|||
|
||||
# java.io.File model (jolt-hjw). io/file and (File. …) build a tagged :jolt/file
|
||||
# value so (instance? File x) works and migratus's File-vs-jar branching takes
|
||||
# the filesystem path. The File method surface + nio glob live in javatime; here
|
||||
# the filesystem path. The File method surface + nio glob live in host_interop; here
|
||||
# are the constructor/predicate builtins and the path coercion str/slurp use.
|
||||
(defn core-file-path
|
||||
"The path string of a :jolt/file, or (string x) for anything else."
|
||||
|
|
|
|||
|
|
@ -534,90 +534,6 @@
|
|||
(set t (table/getproto t)))
|
||||
result)
|
||||
|
||||
(def- math-statics
|
||||
@{"sqrt" math/sqrt "pow" math/pow "floor" math/floor "ceil" math/ceil
|
||||
"abs" (fn [x] (if (< x 0) (- x) x))
|
||||
"round" (fn [x] (math/round x))
|
||||
"sin" math/sin "cos" math/cos "tan" math/tan
|
||||
"asin" math/asin "acos" math/acos "atan" math/atan
|
||||
"log" math/log "log10" math/log10 "exp" math/exp
|
||||
"max" (fn [a b] (if (> a b) a b)) "min" (fn [a b] (if (< a b) a b))
|
||||
"signum" (fn [x] (cond (< x 0) -1.0 (> x 0) 1.0 0.0))
|
||||
"PI" math/pi "E" math/e
|
||||
"random" (fn [&] (math/random))})
|
||||
|
||||
# Thread statics (the JVM shapes portable code actually uses). sleep parks the
|
||||
# CURRENT thread's event loop — inside a future body that's the worker OS
|
||||
# thread (ev/spawn-thread gives each worker its own loop), so a sleeping
|
||||
# future doesn't block the parent.
|
||||
(def- thread-statics
|
||||
{"sleep" (fn [ms] (ev/sleep (/ ms 1000)) nil)
|
||||
"yield" (fn [] (ev/sleep 0) nil)
|
||||
"interrupted" (fn [] false)
|
||||
"currentThread" (fn [] @{:jolt/type :jolt/thread :id "main"})})
|
||||
|
||||
# System statics (wall/monotonic clocks — what portable timing code uses).
|
||||
(def- system-statics
|
||||
# realtime clock (sub-ms float epoch seconds) — os/time is whole seconds,
|
||||
# which quantized every elapsed-time measurement to 1000ms.
|
||||
{"currentTimeMillis" (fn [] (math/floor (* 1000 (os/clock :realtime))))
|
||||
"nanoTime" (fn [] (math/floor (* 1e9 (os/clock :monotonic))))
|
||||
"getProperty" (fn [k &opt dflt]
|
||||
(case k
|
||||
"os.name" (case (os/which)
|
||||
:windows "Windows" :macos "Mac OS X" "Linux")
|
||||
"line.separator" "\n"
|
||||
"file.separator" "/"
|
||||
"user.dir" (os/cwd)
|
||||
"user.home" (os/getenv "HOME")
|
||||
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")
|
||||
dflt))
|
||||
# JOLT_BAKE_ENV_ALLOWLIST (jolt-s3j): during an image bake (jpm build of a
|
||||
# native executable, set by the project's build.sh) the env snapshot that
|
||||
# libraries like config.core capture at load gets MARSHALED INTO THE BINARY
|
||||
# — GitHub push protection once flagged real API tokens inside an example's
|
||||
# build output. With the var set, System/getenv serves only the listed
|
||||
# comma-separated names (single-var reads of unlisted names return nil), so
|
||||
# nothing secret can bake. Unset (the normal runtime case), reads are live
|
||||
# and unfiltered.
|
||||
"getenv" (fn [&opt k]
|
||||
(def allow (os/getenv "JOLT_BAKE_ENV_ALLOWLIST"))
|
||||
(if (nil? allow)
|
||||
(if k (os/getenv k) (os/environ))
|
||||
(let [names (string/split "," allow)
|
||||
ok @{}]
|
||||
(each n names (put ok (string/trim n) true))
|
||||
(if k
|
||||
(when (get ok k) (os/getenv k))
|
||||
(let [e (os/environ) out @{}]
|
||||
(eachp [ek ev] e (when (get ok ek) (put out ek ev)))
|
||||
out)))))
|
||||
# the property subset getProperty serves, as an iterable map
|
||||
"getProperties" (fn []
|
||||
{"os.name" (case (os/which)
|
||||
:windows "Windows" :macos "Mac OS X" "Linux")
|
||||
"line.separator" "\n"
|
||||
"file.separator" "/"
|
||||
"user.dir" (os/cwd)
|
||||
"user.home" (or (os/getenv "HOME") "")
|
||||
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")})})
|
||||
|
||||
# Long statics: sentinels portable code compares against. jolt numbers are
|
||||
# doubles, so these are the f64 approximations.
|
||||
(def- long-statics
|
||||
{"MAX_VALUE" 9223372036854775807
|
||||
"MIN_VALUE" -9223372036854775808
|
||||
"parseLong" (fn [s &opt radix]
|
||||
(def n (scan-number (string/trim (string s)) (or radix 10)))
|
||||
(if (and n (= n (math/floor n)))
|
||||
n
|
||||
(error (string "NumberFormatException: For input string: \"" s "\""))))
|
||||
"valueOf" (fn [s &opt radix]
|
||||
(def n (scan-number (string/trim (string s)) (or radix 10)))
|
||||
(if (and n (= n (math/floor n)))
|
||||
n
|
||||
(error (string "NumberFormatException: For input string: \"" s "\""))))})
|
||||
|
||||
# Pluggable host-class shims (java.time etc. register here at module load):
|
||||
# class-statics: "ClassName" -> {"member" value-or-fn} (Foo/bar resolution)
|
||||
# tagged-methods: :jolt/tag -> {"method" (fn [self args...])} ((.m obj) dispatch)
|
||||
|
|
@ -777,18 +693,8 @@
|
|||
(defn- resolve-sym
|
||||
[ctx bindings sym-s]
|
||||
(let [name (sym-s :name) ns (sym-s :ns)]
|
||||
(if (= ns "Math")
|
||||
(let [v (get math-statics name)]
|
||||
(if (nil? v) (error (string "Unsupported Math member: Math/" name)) v))
|
||||
(if (= ns "Thread")
|
||||
(let [v (get thread-statics name)]
|
||||
(if (nil? v) (error (string "Unsupported Thread member: Thread/" name)) v))
|
||||
(if (= ns "System")
|
||||
(let [v (get system-statics name)]
|
||||
(if (nil? v) (error (string "Unsupported System member: System/" name)) v))
|
||||
(if (= ns "Long")
|
||||
(let [v (get long-statics name)]
|
||||
(if (nil? v) (error (string "Unsupported Long member: Long/" name)) v))
|
||||
# Math/Thread/System/Long and every other class resolve through the generic
|
||||
# class-statics registry (host_interop registers them at load); no special-case.
|
||||
(if (get class-statics ns)
|
||||
(let [v (get (get class-statics ns) name)]
|
||||
(if (nil? v) (error (string "Unsupported member: " ns "/" name)) v))
|
||||
|
|
@ -859,7 +765,7 @@
|
|||
# janet/ prefix above.
|
||||
(if (or (in class-ctors name) (get class-canonical-names name) (get class-value-overrides name))
|
||||
(class-value-for name)
|
||||
(error (string "Unable to resolve symbol: " name " in this context")))))))))))))))))))
|
||||
(error (string "Unable to resolve symbol: " name " in this context")))))))))))))))
|
||||
(defn- parse-arg-names
|
||||
"Parse a parameter vector, handling & rest args.
|
||||
Returns {:fixed [names...] :rest name-or-nil :all [names...]}"
|
||||
|
|
@ -1778,7 +1684,7 @@
|
|||
"java.util.regex.Pattern" (and (table? val) (= :jolt/regex (val :jolt/type)))
|
||||
"Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
|
||||
"java.lang.Character" (and (struct? val) (= :jolt/char (get val :jolt/type)))
|
||||
# java.time shims (javatime.janet); #inst IS java.util.Date in Clojure
|
||||
# java.time shims (host_interop.janet); #inst IS java.util.Date in Clojure
|
||||
"java.util.Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
|
||||
"Date" (and (struct? val) (= :jolt/inst (get val :jolt/type)))
|
||||
"Instant" (and (table? val) (= :jolt/instant (get val :jolt/type)))
|
||||
|
|
|
|||
|
|
@ -1,16 +1,110 @@
|
|||
# java.time shims (jolt-ea7): the surface Selmer's date filters use, backed
|
||||
# by epoch milliseconds (the same representation as :jolt/inst). Local time
|
||||
# means the HOST's local time (os/date with local=true); zones beyond the
|
||||
# system default are not modeled. Registered through the evaluator's
|
||||
# class-statics / tagged-methods registries, so this module is data plus an
|
||||
# install call — adding another java.* shim follows the same shape.
|
||||
# Host interop: the JVM class/method surface that portable cljc libraries call,
|
||||
# emulated over jolt values — java.time, java.io, java.util, java.net, java.lang
|
||||
# (Math/System/Thread/Long/...), java.sql and a few clojure.lang shims, plus the
|
||||
# wiring that lets Java-style collection interop (.iterator/.nth/.count) and
|
||||
# malli's collection-builder statics work over jolt collections.
|
||||
#
|
||||
# Everything registers through the evaluator's class-statics / tagged-methods /
|
||||
# class-ctors registries, so each shim is data + an install call: adding a
|
||||
# `(SomeClass. ...)` ctor or a `.someMethod` follows the same shape. This is the
|
||||
# one home for host shims (jolt-jx5l); the registry machinery stays in evaluator.
|
||||
|
||||
(use ./evaluator)
|
||||
(use ./regex)
|
||||
(use ./core)
|
||||
(use ./pv)
|
||||
(use ./plist)
|
||||
(use ./types)
|
||||
(import ./phm)
|
||||
|
||||
(defn- chr [s] (get s 0))
|
||||
|
||||
# --- java.lang static surfaces (Math/Thread/System/Long) ----------------------
|
||||
# Registered through the generic class-statics registry below, same as every
|
||||
# other class — there is no special-case dispatch (jolt-jx5l).
|
||||
(def- math-statics
|
||||
@{"sqrt" math/sqrt "pow" math/pow "floor" math/floor "ceil" math/ceil
|
||||
"abs" (fn [x] (if (< x 0) (- x) x))
|
||||
"round" (fn [x] (math/round x))
|
||||
"sin" math/sin "cos" math/cos "tan" math/tan
|
||||
"asin" math/asin "acos" math/acos "atan" math/atan
|
||||
"log" math/log "log10" math/log10 "exp" math/exp
|
||||
"max" (fn [a b] (if (> a b) a b)) "min" (fn [a b] (if (< a b) a b))
|
||||
"signum" (fn [x] (cond (< x 0) -1.0 (> x 0) 1.0 0.0))
|
||||
"PI" math/pi "E" math/e
|
||||
"random" (fn [&] (math/random))})
|
||||
|
||||
# sleep parks the CURRENT thread's event loop — inside a future body that's the
|
||||
# worker OS thread (ev/spawn-thread gives each worker its own loop), so a
|
||||
# sleeping future doesn't block the parent.
|
||||
(def- thread-statics
|
||||
{"sleep" (fn [ms] (ev/sleep (/ ms 1000)) nil)
|
||||
"yield" (fn [] (ev/sleep 0) nil)
|
||||
"interrupted" (fn [] false)
|
||||
"currentThread" (fn [] @{:jolt/type :jolt/thread :id "main"})})
|
||||
|
||||
# wall/monotonic clocks + properties/env (what portable timing/config code uses).
|
||||
(def- system-statics
|
||||
# realtime clock (sub-ms float epoch seconds) — os/time is whole seconds,
|
||||
# which quantized every elapsed-time measurement to 1000ms.
|
||||
{"currentTimeMillis" (fn [] (math/floor (* 1000 (os/clock :realtime))))
|
||||
"nanoTime" (fn [] (math/floor (* 1e9 (os/clock :monotonic))))
|
||||
"getProperty" (fn [k &opt dflt]
|
||||
(case k
|
||||
"os.name" (case (os/which)
|
||||
:windows "Windows" :macos "Mac OS X" "Linux")
|
||||
"line.separator" "\n"
|
||||
"file.separator" "/"
|
||||
"user.dir" (os/cwd)
|
||||
"user.home" (os/getenv "HOME")
|
||||
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")
|
||||
dflt))
|
||||
# JOLT_BAKE_ENV_ALLOWLIST (jolt-s3j): during an image bake (jpm build of a
|
||||
# native executable, set by the project's build.sh) the env snapshot that
|
||||
# libraries like config.core capture at load gets MARSHALED INTO THE BINARY
|
||||
# — GitHub push protection once flagged real API tokens inside an example's
|
||||
# build output. With the var set, System/getenv serves only the listed
|
||||
# comma-separated names (single-var reads of unlisted names return nil), so
|
||||
# nothing secret can bake. Unset (the normal runtime case), reads are live
|
||||
# and unfiltered.
|
||||
"getenv" (fn [&opt k]
|
||||
(def allow (os/getenv "JOLT_BAKE_ENV_ALLOWLIST"))
|
||||
(if (nil? allow)
|
||||
(if k (os/getenv k) (os/environ))
|
||||
(let [names (string/split "," allow)
|
||||
ok @{}]
|
||||
(each n names (put ok (string/trim n) true))
|
||||
(if k
|
||||
(when (get ok k) (os/getenv k))
|
||||
(let [e (os/environ) out @{}]
|
||||
(eachp [ek ev] e (when (get ok ek) (put out ek ev)))
|
||||
out)))))
|
||||
# the property subset getProperty serves, as an iterable map
|
||||
"getProperties" (fn []
|
||||
{"os.name" (case (os/which)
|
||||
:windows "Windows" :macos "Mac OS X" "Linux")
|
||||
"line.separator" "\n"
|
||||
"file.separator" "/"
|
||||
"user.dir" (os/cwd)
|
||||
"user.home" (or (os/getenv "HOME") "")
|
||||
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")})})
|
||||
|
||||
# sentinels portable code compares against. jolt numbers are doubles, so these
|
||||
# are the f64 approximations.
|
||||
(def- long-statics
|
||||
{"MAX_VALUE" 9223372036854775807
|
||||
"MIN_VALUE" -9223372036854775808
|
||||
"parseLong" (fn [s &opt radix]
|
||||
(def n (scan-number (string/trim (string s)) (or radix 10)))
|
||||
(if (and n (= n (math/floor n)))
|
||||
n
|
||||
(error (string "NumberFormatException: For input string: \"" s "\""))))
|
||||
"valueOf" (fn [s &opt radix]
|
||||
(def n (scan-number (string/trim (string s)) (or radix 10)))
|
||||
(if (and n (= n (math/floor n)))
|
||||
n
|
||||
(error (string "NumberFormatException: For input string: \"" s "\""))))})
|
||||
|
||||
# --- values -------------------------------------------------------------------
|
||||
|
||||
(defn- instant [ms] @{:jolt/type :jolt/instant :ms ms})
|
||||
|
|
@ -112,6 +206,11 @@
|
|||
# --- registration --------------------------------------------------------------
|
||||
|
||||
(defn install! []
|
||||
# java.lang statics through the generic registry (no resolve-sym special-case).
|
||||
(register-class-statics! "Math" math-statics)
|
||||
(register-class-statics! "Thread" thread-statics)
|
||||
(register-class-statics! "System" system-statics)
|
||||
(register-class-statics! "Long" long-statics)
|
||||
(def fs (fn [style] @{:jolt/type :jolt/format-style :style style}))
|
||||
(register-class-statics! "FormatStyle"
|
||||
@{"SHORT" (fs :short) "MEDIUM" (fs :medium) "LONG" (fs :long) "FULL" (fs :full)})
|
||||
|
|
@ -670,5 +769,44 @@
|
|||
(register-tagged-methods! :jolt/nio-matcher
|
||||
@{"matches" (fn [self path] (glob-matches? (get self :glob) (get path :s)))}))
|
||||
|
||||
# Collection interop: wires the evaluator's late-bound hooks to core's
|
||||
# collection ops so Java-style interop works over jolt values (moved here from
|
||||
# api — jolt-jx5l). Late-bound because the evaluator loads before core.
|
||||
(defn install-collections! []
|
||||
# (.iterator coll) -> a jolt iterator over any seqable (hiccup's iterate!).
|
||||
(set-coll-realizer! realize-for-iteration)
|
||||
# clj-targeted libs build collections via JVM statics in their :clj branches;
|
||||
# malli's entry parser uses these. createOwning takes ownership of an object
|
||||
# array -> a vector; createWithCheck builds a map and throws on duplicate keys
|
||||
# (malli catches that), detected by the built map being smaller than the kvs.
|
||||
(register-class-statics! "LazilyPersistentVector"
|
||||
@{"createOwning" (fn [arr] (make-vec arr))})
|
||||
(register-class-statics! "PersistentArrayMap"
|
||||
@{"createWithCheck"
|
||||
(fn [arr]
|
||||
(def m (phm/make-phm arr))
|
||||
(if (= (* 2 (phm/phm-count m)) (length arr)) m
|
||||
(error "PersistentArrayMap: duplicate key")))})
|
||||
# .nth/.count/.valAt/.get/.seq/.containsKey on a jolt collection -> the
|
||||
# clojure.core equivalent. :jolt/ci-none means "not a collection method here".
|
||||
(set-coll-interop!
|
||||
(fn [target name args]
|
||||
(if-not (or (pvec? target) (phm/phm? target) (plist? target) (phm/lazy-seq? target)
|
||||
(and (table? target) (= :jolt/set (get target :jolt/type)))
|
||||
(shape-rec? target) # map-as-tuple record
|
||||
(and (struct? target) (nil? (get target :jolt/type)))) # plain map literal
|
||||
:jolt/ci-none
|
||||
(cond
|
||||
(= name "nth") (if (>= (length args) 2) (core-nth target (in args 0) (in args 1))
|
||||
(core-nth target (in args 0)))
|
||||
(= name "count") (core-count target)
|
||||
(or (= name "valAt") (= name "get"))
|
||||
(if (>= (length args) 2) (core-get target (in args 0) (in args 1))
|
||||
(core-get target (in args 0)))
|
||||
(= name "seq") (core-seq target)
|
||||
(= name "containsKey") (core-contains? target (in args 0))
|
||||
:jolt/ci-none)))))
|
||||
|
||||
(install!)
|
||||
(install-io!)
|
||||
(install-collections!)
|
||||
Loading…
Add table
Add a link
Reference in a new issue