diff --git a/src/jolt/api.janet b/src/jolt/api.janet index fb14cb7..85cf467 100644 --- a/src/jolt/api.janet +++ b/src/jolt/api.janet @@ -326,6 +326,22 @@ (when (not (empty? idxs)) (string/slice api-module-file 0 (last idxs)))))) +# Every .janet seed file under `dir`, RECURSIVELY (src/jolt/ has subdirs now — +# interop/), keyed by repo-relative path so files in different dirs don't alias. +# Sorted for a stable fingerprint. Non-recursive (os/dir) would silently miss a +# subdir edit and serve a stale image — the map-nil-style footgun, for the cache. +(defn- janet-sources-under [dir] + (def acc @[]) + (defn- walk [d prefix] + (each name (sorted (os/dir d)) + (def full (string d "/" name)) + (def rel (string prefix name)) + (case (os/stat full :mode) + :directory (walk full (string rel "/")) + :file (when (string/has-suffix? ".janet" name) (array/push acc [rel full]))))) + (walk dir "") + acc) + (defn- source-fingerprint "Hash + total length of every source a fresh init depends on. Two numbers, so a 32-bit hash collision alone can't alias two different source trees." @@ -335,9 +351,8 @@ (buffer/push buf k "\x00" (get stdlib-embed/sources k) "\x00")) (def dir (src-dir)) (when dir - (each f (sorted (os/dir dir)) - (when (string/has-suffix? ".janet" f) - (buffer/push buf f "\x00" (slurp (string dir "/" f)) "\x00")))) + (each [rel full] (janet-sources-under dir) + (buffer/push buf rel "\x00" (slurp full) "\x00"))) [(hash (string buf)) (length buf)]) (defn- image-cache-path [opts] diff --git a/src/jolt/host_interop.janet b/src/jolt/host_interop.janet index 9038837..1bd9b41 100644 --- a/src/jolt/host_interop.janet +++ b/src/jolt/host_interop.janet @@ -1,812 +1,18 @@ -# 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. +# Host interop aggregator (jolt-jx5l, phase 1 subdir split). # -# 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) -(use ./lazyseq) -(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}) -(defn- zoned [ms zone] @{:jolt/type :jolt/zoned-dt :ms ms :zone zone}) -(defn- local-dt [ms] @{:jolt/type :jolt/local-dt :ms ms}) -(defn- formatter [pattern &opt locale] @{:jolt/type :jolt/dt-formatter :pattern pattern :locale locale}) - -(def- zone-default @{:jolt/type :jolt/zone-id :id "system"}) - -# ms of any date-ish shim value (or a :jolt/inst) -(defn- ms-of [d] - (cond - (number? d) d - (and (or (table? d) (struct? d)) - (or (= :jolt/inst (get d :jolt/type)) - (= :jolt/instant (get d :jolt/type)) - (= :jolt/zoned-dt (get d :jolt/type)) - (= :jolt/local-dt (get d :jolt/type)))) - (get d :ms) - (error (string "not a date value: " (type d))))) - -# --- formatting ---------------------------------------------------------------- - -(def- month-names ["January" "February" "March" "April" "May" "June" "July" - "August" "September" "October" "November" "December"]) -(def- day-names ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"]) - -(defn- pad2 [n] (if (< n 10) (string "0" n) (string n))) - -# Format epoch-ms with a (subset of the) JVM DateTimeFormatter pattern: -# yyyy yy MMMM MMM MM M dd d EEEE EEE HH H hh h mm m ss s a, quoted literals -# with '...'. Unknown letters pass through. -(defn- format-ms [pattern ms] - (def d (os/date (math/floor (/ ms 1000)) true)) - (def out @"") - (var i 0) - (def n (length pattern)) - (defn run-len [c] - (var j i) - (while (and (< j n) (= (pattern j) c)) (++ j)) - (- j i)) - (while (< i n) - (def c (pattern i)) - (def k (run-len c)) - (cond - (= c (chr "'")) - # quoted literal up to the closing quote ('' = literal quote) - (if (and (< (+ i 1) n) (= (pattern (+ i 1)) (chr "'"))) - (do (buffer/push out "'") (+= i 2)) - (let [close (string/find "'" pattern (+ i 1))] - (buffer/push out (string/slice pattern (+ i 1) close)) - (set i (+ close 1)))) - (= c (chr "y")) - (do (buffer/push out (if (>= k 4) (string (d :year)) - (pad2 (mod (d :year) 100)))) - (+= i k)) - (= c (chr "M")) - (do (buffer/push out (case k - 1 (string (+ 1 (d :month))) - 2 (pad2 (+ 1 (d :month))) - 3 (string/slice (in month-names (d :month)) 0 3) - (in month-names (d :month)))) - (+= i k)) - (= c (chr "d")) - (do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day))))) - (+= i k)) - (= c (chr "E")) - (do (buffer/push out (if (>= k 4) (in day-names (d :week-day)) - (string/slice (in day-names (d :week-day)) 0 3))) - (+= i k)) - (= c (chr "H")) - (do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k)) - (= c (chr "h")) - (let [h12 (let [h (mod (d :hours) 12)] (if (= h 0) 12 h))] - (buffer/push out (if (= k 1) (string h12) (pad2 h12))) (+= i k)) - (= c (chr "m")) - (do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k)) - (= c (chr "s")) - (do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k)) - (= c (chr "a")) - (do (buffer/push out (if (< (d :hours) 12) "AM" "PM")) (+= i k)) - (do (buffer/push out (string/from-bytes c)) (++ i)))) - (string out)) - -# Localized FormatStyle approximations (no locale database on this host). -(def- style-patterns - {[:date :short] "M/d/yy" [:date :medium] "MMM d, yyyy" - [:date :long] "MMMM d, yyyy" [:date :full] "EEEE, MMMM d, yyyy" - [:time :short] "h:mm a" [:time :medium] "h:mm:ss a" - [:time :long] "h:mm:ss a" [:time :full] "h:mm:ss a" - [:datetime :short] "M/d/yy, h:mm a" - [:datetime :medium] "MMM d, yyyy, h:mm:ss a" - [:datetime :long] "MMMM d, yyyy, h:mm:ss a" - [:datetime :full] "EEEE, MMMM d, yyyy, h:mm:ss a"}) - -(defn- style-fmt [kind style] - (formatter (get style-patterns [kind (get style :style)] "yyyy-MM-dd HH:mm:ss"))) - -# --- 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)}) - (register-class-statics! "DateTimeFormatter" - @{"ofPattern" (fn [p &opt locale] (formatter p locale)) - "ISO_LOCAL_DATE" (formatter "yyyy-MM-dd") - "ISO_LOCAL_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ss") - "ofLocalizedDate" (fn [style] (style-fmt :date style)) - "ofLocalizedTime" (fn [style] (style-fmt :time style)) - "ofLocalizedDateTime" (fn [style] (style-fmt :datetime style))}) - (register-class-statics! "Instant" - @{"ofEpochMilli" (fn [ms] (instant ms)) - "now" (fn [] (instant (math/floor (* 1000 (os/clock :realtime)))))}) - (register-class-statics! "ZoneId" - @{"systemDefault" (fn [] zone-default)}) - (register-class-statics! "LocalDateTime" - @{"ofInstant" (fn [inst zone] (local-dt (ms-of inst))) - "now" (fn [] (local-dt (math/floor (* 1000 (os/clock :realtime)))))}) - (let [locale-statics @{"getDefault" (fn [] @{:jolt/type :jolt/locale :id "default"}) - "ENGLISH" @{:jolt/type :jolt/locale :id "en"} - "US" @{:jolt/type :jolt/locale :id "en-US"} - "ROOT" @{:jolt/type :jolt/locale :id "root"}}] - (each nm ["Locale" "java.util.Locale"] - (register-class-statics! nm locale-statics))) - (register-tagged-methods! :jolt/instant - @{"atZone" (fn [self zone] (zoned (self :ms) zone)) - "toEpochMilli" (fn [self] (self :ms))}) - (register-tagged-methods! :jolt/zoned-dt - @{"toLocalDateTime" (fn [self] (local-dt (self :ms))) - "toInstant" (fn [self] (instant (self :ms)))}) - (register-tagged-methods! :jolt/local-dt - @{"atZone" (fn [self zone] (zoned (self :ms) zone))}) - # a :jolt/inst (#inst — Clojure's java.util.Date) supports the Date methods - # Selmer's fix-date path calls - (register-tagged-methods! :jolt/inst - @{"toInstant" (fn [self] (instant (self :ms))) - "getTime" (fn [self] (self :ms))}) - (register-tagged-methods! :jolt/dt-formatter - @{"withLocale" (fn [self locale] (formatter (self :pattern) locale)) - "format" (fn [self d] (format-ms (self :pattern) (ms-of d)))})) - -# --- java.io / java.lang shims (Selmer's template reader) --------------------- - -(defn- string-reader [src] - # :close makes with-open's __close happy (it calls (get x :close) when - # present); :read-line-fn matches the 50-io reader convention so line-seq - # works over readers io/reader hands back - (def self @{:jolt/type :jolt/jio-string-reader :s (string src) :pos 0 - :close (fn [] nil)}) - (put self :read-line-fn - (fn [] - (def {:s s :pos pos} self) - (when (< pos (length s)) - (def i (string/find "\n" s pos)) - (if (nil? i) - (do (put self :pos (length s)) (string/slice s pos)) - (do (put self :pos (+ i 1)) (string/slice s pos i)))))) - self) -(defn- string-builder [&opt init] - # a numeric arg is a CAPACITY (java.lang.StringBuilder(int)), not content - @{:jolt/type :jolt/string-builder - :buf (cond (nil? init) @"" (number? init) (buffer/new init) (buffer init))}) - -(defn make-string-writer [] - # :close lets with-open close the writer (core-close-resource calls :close); - # it's a no-op so .toString after with-open still sees the buffer. - @{:jolt/type :jolt/writer :buf @"" :sink nil :close (fn [] nil)}) -(defn make-out-writer [] - @{:jolt/type :jolt/writer :buf nil :sink prin}) - -(defn- render-piece [x] - (cond - (nil? x) "null" - (and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch)) - (string x))) - -# Writer.write(int) writes the CHAR for that code (unlike StringBuilder.append(int), -# which appends the int's digits). jolt chars are bytes, so this round-trips UTF-8 -# byte-for-byte with readLine. -(defn- writer-piece [x] - (if (number? x) (string/from-bytes (math/trunc x)) (render-piece x))) - -# Read one unit from any reader-ish value: our shims dispatch through their -# tagged "read"; a janet core/file reads one byte. -1 at EOF. -(defn- read-unit [r] - (cond - (and (or (table? r) (struct? r)) (get r :jolt/type)) - (((get tagged-methods (r :jolt/type)) "read") r) - (= :core/file (type r)) - (let [b (file/read r 1)] (if (or (nil? b) (= 0 (length b))) -1 (b 0))) - (error (string "not a reader: " (type r))))) - -(defn- pushback-reader [rdr] - # java.io.PushbackReader: read delegates to the wrapped reader unless - # something was unread; unread takes a char (or char code) and pushes it back - (def self @{:jolt/type :jolt/pushback-reader :rdr rdr :pushed @[] - :close (fn [] nil)}) - self) - -(defn install-io! [] - (register-tagged-methods! :jolt/pushback-reader - @{"read" (fn [self] - (if (> (length (self :pushed)) 0) - (array/pop (self :pushed)) - (read-unit (self :rdr)))) - "unread" (fn [self ch] - (array/push (self :pushed) - (if (number? ch) ch (get ch :ch))) - nil) - "close" (fn [self] nil)}) - (register-tagged-methods! :jolt/jio-string-reader - @{"read" (fn [self] - (if (>= (self :pos) (length (self :s))) - -1 - (let [b ((self :s) (self :pos))] - (put self :pos (+ 1 (self :pos))) - b))) - "readLine" (fn [self] ((self :read-line-fn))) - "mark" (fn [self &opt limit] (put self :marked (self :pos)) nil) - "reset" (fn [self] (put self :pos (or (self :marked) 0)) nil) - "skip" (fn [self n] (put self :pos (min (length (self :s)) (+ (self :pos) n))) n) - "close" (fn [self] nil)}) - # java.io.Writer / StringWriter — the print-method protocol surface - # (jolt-g1r). A writer either pushes to a sink fn (stdout/custom) or - # accumulates in a buffer (StringWriter). write/append coerce chars the - # same way StringBuilder does. - (register-tagged-methods! :jolt/writer - @{"write" (fn [self x] - (if (self :sink) - ((self :sink) (writer-piece x)) - (buffer/push-string (self :buf) (writer-piece x))) - nil) - "append" (fn [self x] - (if (self :sink) - ((self :sink) (render-piece x)) - (buffer/push-string (self :buf) (render-piece x))) - self) - "flush" (fn [self] nil) - "close" (fn [self] nil) - "toString" (fn [self] (string (or (self :buf) "")))}) - (register-tagged-methods! :jolt/string-builder - @{"append" (fn [self x] (buffer/push (self :buf) (render-piece x)) self) - "toString" (fn [self] (string (self :buf))) - "length" (fn [self] (length (self :buf))) - "setLength" (fn [self n] - (def buf (self :buf)) - (if (< n (length buf)) - (buffer/popn buf (- (length buf) n)) - (while (< (length buf) n) (buffer/push buf "\0"))) - nil) - "charAt" (fn [self i] {:jolt/type :jolt/char :ch ((self :buf) i)})}) - (each nm ["File" "java.io.File"] - (register-class-statics! nm @{"separator" "/" "separatorChar" {:jolt/type :jolt/char :ch 47}})) - (register-class-statics! "Boolean" - @{"parseBoolean" (fn [s] (= "true" (string/ascii-lower (string s)))) - "TRUE" true "FALSE" false}) - (register-class-statics! "Class" - @{"forName" (fn [nm] @{:jolt/type :jolt/class :name nm})}) - (each nm ["StringReader" "java.io.StringReader"] - (register-class-ctor! nm string-reader)) - (each nm ["StringBuilder" "java.lang.StringBuilder"] - (register-class-ctor! nm string-builder)) - (each nm ["StringWriter" "java.io.StringWriter"] - (register-class-ctor! nm make-string-writer)) - # --- java.net / java.util surface for ring-codec (ring-core enablement) --- - # URLEncoder/URLDecoder: www-form-urlencoded (space <-> +, %XX bytes, - # [A-Za-z0-9.*_-] literal). Charset args are accepted and ignored beyond - # the name (everything is UTF-8 bytes here). - (defn- url-unreserved? [b] - (or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90)) - (and (>= b 97) (<= b 122)) (= b 46) (= b 42) (= b 95) (= b 45))) - (defn- url-encode-www [s & _] - (def out @"") - (each b (string/bytes (string s)) - (cond - (url-unreserved? b) (buffer/push-byte out b) - (= b 32) (buffer/push-string out "+") - (buffer/push-string out (string/format "%%%02X" b)))) - (string out)) - (defn- hexv [b] - (cond (and (>= b 48) (<= b 57)) (- b 48) - (and (>= b 65) (<= b 70)) (- b 55) - (and (>= b 97) (<= b 102)) (- b 87) - (error "URLDecoder: malformed escape"))) - (defn- url-decode-www [s & _] - (def bytes (string/bytes (string s))) - (def n (length bytes)) - (def out @"") - (var i 0) - (while (< i n) - (def b (in bytes i)) - (cond - (= b 43) (do (buffer/push-string out " ") (++ i)) - (= b 37) (if (< (+ i 2) n) - (do (buffer/push-byte out (+ (* 16 (hexv (in bytes (+ i 1)))) (hexv (in bytes (+ i 2))))) - (+= i 3)) - (error "URLDecoder: incomplete escape")) - (do (buffer/push-byte out b) (++ i)))) - (string out)) - (each nm ["URLEncoder" "java.net.URLEncoder"] - (register-class-statics! nm @{"encode" url-encode-www})) - (each nm ["URLDecoder" "java.net.URLDecoder"] - (register-class-statics! nm @{"decode" url-decode-www})) - (each nm ["Charset" "java.nio.charset.Charset"] - (register-class-statics! nm @{"forName" (fn [nm*] @{:jolt/type :jolt/charset :name nm*})})) - # Base64 (RFC 4648): encoder/decoder singletons with encode/decode methods. - (def- b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") - (defn- b64-encode [bs] - (def bytes (if (bytes? bs) bs (string bs))) - (def n (length bytes)) - (def out @"") - (var i 0) - (while (< i n) - (def b0 (in bytes i)) - (def b1 (if (< (+ i 1) n) (in bytes (+ i 1)))) - (def b2 (if (< (+ i 2) n) (in bytes (+ i 2)))) - (buffer/push-byte out (in b64-alphabet (brshift b0 2))) - (buffer/push-byte out (in b64-alphabet (bor (blshift (band b0 3) 4) (brshift (or b1 0) 4)))) - (buffer/push-string out (if (nil? b1) "=" (string/from-bytes (in b64-alphabet (bor (blshift (band b1 0xf) 2) (brshift (or b2 0) 6)))))) - (buffer/push-string out (if (nil? b2) "=" (string/from-bytes (in b64-alphabet (band b2 0x3f))))) - (+= i 3)) - (string out)) - (def- b64-rev (do (def t @{}) (eachp [i c] b64-alphabet (put t c i)) t)) - (defn- b64-decode [s] - (def cleaned (string/replace-all "=" "" (string s))) - (def out @"") - (var acc 0) (var bits 0) - (each c (string/bytes cleaned) - (def v (get b64-rev c)) - (when (nil? v) (error "Base64: illegal character")) - (set acc (bor (blshift acc 6) v)) - (+= bits 6) - (when (>= bits 8) - (-= bits 8) - (buffer/push-byte out (band (brshift acc bits) 0xff)))) - out) - (register-tagged-methods! :jolt/base64-encoder @{"encode" (fn [self bs] (b64-encode bs)) "encodeToString" (fn [self bs] (b64-encode bs))}) - (register-tagged-methods! :jolt/base64-decoder @{"decode" (fn [self s] (b64-decode s))}) - (each nm ["Base64" "java.util.Base64"] - (register-class-statics! nm - @{"getEncoder" (fn [] @{:jolt/type :jolt/base64-encoder}) - "getDecoder" (fn [] @{:jolt/type :jolt/base64-decoder})})) - # Integer statics: valueOf with optional radix. Returns a plain number — - # byteValue/intValue live on the number method surface in the evaluator. - (register-class-statics! "Integer" - @{"valueOf" (fn [x &opt radix] - (cond - (number? x) x - (nil? radix) (or (scan-number (string x)) (error (string "NumberFormatException: " x))) - (= radix 16) (or (scan-number (string "16r" x)) (error (string "NumberFormatException: " x))) - (= radix 8) (or (scan-number (string "8r" x)) (error (string "NumberFormatException: " x))) - (= radix 2) (or (scan-number (string "2r" x)) (error (string "NumberFormatException: " x))) - (error (string "Integer/valueOf: unsupported radix " radix)))) - "parseInt" (fn [x &opt radix] - (or (scan-number (string (case radix 16 "16r" 8 "8r" 2 "2r" "") x)) - (error (string "NumberFormatException: " x)))) - "MAX_VALUE" 2147483647 - "MIN_VALUE" -2147483648}) - # StringTokenizer: eager split on any delimiter char, empty tokens skipped. - (defn- tokenize [s delims] - (def dset @{}) - (each b (string/bytes delims) (put dset b true)) - (def toks @[]) - (def cur @"") - (each b (string/bytes (string s)) - (if (get dset b) - (when (> (length cur) 0) (array/push toks (string cur)) (buffer/clear cur)) - (buffer/push-byte cur b))) - (when (> (length cur) 0) (array/push toks (string cur))) - toks) - (register-tagged-methods! :jolt/string-tokenizer - @{"hasMoreTokens" (fn [self] (< (self :pos) (length (self :toks)))) - "countTokens" (fn [self] (- (length (self :toks)) (self :pos))) - "nextToken" (fn [self] - (if (< (self :pos) (length (self :toks))) - (let [t (in (self :toks) (self :pos))] - (put self :pos (+ 1 (self :pos))) t) - (error "NoSuchElementException")))}) - (each nm ["StringTokenizer" "java.util.StringTokenizer"] - (register-class-ctor! nm (fn [s &opt delims] - @{:jolt/type :jolt/string-tokenizer - :toks (tokenize s (or delims " \t\n\r\f")) - :pos 0}))) - # clojure.lang.MapEntry: a 2-tuple, jolt's map-entry representation. - # java.util.HashMap: a mutable wrapper over a janet table, keyed by canonical - # key (so jolt collection keys compare by value). reitit uses it as a fast - # read cache: (HashMap. m) copies a map's entries, (.get hm k) reads. - # raw value-keys: reitit's HashMap keys are strings/keywords/tuples, all of - # which janet tables key by value — no canonicalization needed here. - (defn- hm-entries [m] - (cond (phm/phm? m) (phm/phm-entries m) - (struct? m) (pairs m) - (table? m) (pairs m) - @[])) - (register-tagged-methods! :jolt/hashmap - @{"get" (fn [self k] (get (self :tbl) k)) - "put" (fn [self k v] (put (self :tbl) k v) v) - "putAll" (fn [self m] (each pair (hm-entries m) (put (self :tbl) (in pair 0) (in pair 1))) nil) - "containsKey" (fn [self k] (not (nil? (get (self :tbl) k)))) - "size" (fn [self] (length (self :tbl)))}) - (each nm ["HashMap" "java.util.HashMap"] - # java.util.HashMap constructors: (), (initialCapacity), (initialCapacity, - # loadFactor) all start empty; (Map m) copies entries. A numeric first arg is - # a capacity (sizing hint we ignore), not content. - (register-class-ctor! nm - (fn [& args] - (def tbl @{}) - (def init (if (> (length args) 0) (in args 0) nil)) - (when (and init (not (number? init))) - (each pair (hm-entries init) (put tbl (in pair 0) (in pair 1)))) - @{:jolt/type :jolt/hashmap :tbl tbl}))) - (each nm ["MapEntry" "clojure.lang.MapEntry"] - (register-class-ctor! nm (fn [k v] [k v]))) - # (String. bytes) / (String. bytes charset): UTF-8 bytes to string. - (each nm ["String" "java.lang.String"] - (register-class-ctor! nm (fn [x &opt charset] (string x)))) - # java.net.URL: enough for selmer's template cache — file: URLs only. - # A protocol-less spec throws (selmer catches MalformedURLException and - # prepends file:///), and getPath hands back a stat-able filesystem path. - (defn url-path [spec] - (var p (if (string/has-prefix? "file:" spec) (string/slice spec 5) spec)) - (while (and (> (length p) 1) (string/has-prefix? "//" p)) - (set p (string/slice p 1))) - p) - (register-tagged-methods! :jolt/url - @{"getPath" (fn [self] (url-path (self :spec))) - "getFile" (fn [self] (url-path (self :spec))) - "toString" (fn [self] (self :spec)) - "toExternalForm" (fn [self] (self :spec))}) - (each nm ["URL" "java.net.URL"] - (register-class-ctor! nm - (fn [spec & _] - (def s (string spec)) - (def colon (string/find ":" s)) - (if (or (nil? colon) (= colon 0) - (string/find "/" (string/slice s 0 colon))) - (error (string "MalformedURLException: no protocol: " s)) - @{:jolt/type :jolt/url :spec s})))) - (each nm ["PushbackReader" "java.io.PushbackReader"] - (register-class-ctor! nm (fn [rdr &opt size] (pushback-reader rdr)))) - (each nm ["BigInteger" "java.math.BigInteger"] - (register-class-ctor! nm - (fn [v] - (or (scan-number (string/trim (string v))) - (error (string "NumberFormatException: For input string: \"" v "\"")))))) - (each nm ["Locale" "java.util.Locale"] - (register-class-ctor! nm (fn [id &opt _country] @{:jolt/type :jolt/locale :id (string id)}))) - # java.util.regex.Pattern statics: Pattern/compile, Pattern/quote, Pattern/MULTILINE. - # Pattern/compile returns jolt's native :jolt/regex compiled value so that - # str/replace, re-matches, .split etc accept it transparently. - (defn- pattern-quote [s] - (def meta "\\.[]{}()*+-?^$|&") - (def buf @"") - (var i 0) - (while (< i (length s)) - (def c (s i)) - (if (string/find (string/from-bytes c) meta) - (buffer/push buf (chr "\\"))) - (buffer/push buf (string/from-bytes c)) - (++ i)) - (string buf)) - (def pattern-multiline 8) - (each nm ["Pattern" "java.util.regex.Pattern"] - (register-class-statics! nm - @{"compile" (fn [s &opt flags] - (if (and flags (= (band flags pattern-multiline) pattern-multiline)) - (re-pattern (string "(?m)" s)) - (re-pattern s))) - "quote" (fn [s] (pattern-quote s)) - "MULTILINE" pattern-multiline})) - # .split on compiled regex values: delegates to re-split, drops trailing empties - (register-tagged-methods! :jolt/regex - @{"split" (fn [self s &opt limit] - (def parts (re-split self s)) - (while (and (> (length parts) 0) (= "" (last parts))) - (array/pop parts)) - parts)}) - # JVM exception constructors: (Exception. msg), (IllegalArgumentException. msg), - # (InterruptedException. msg). Return the message string so getMessage works. - (each nm ["Exception" "java.lang.Exception"] - (register-class-ctor! nm (fn [msg] (string msg)))) - (each nm ["IllegalArgumentException" "java.lang.IllegalArgumentException"] - (register-class-ctor! nm (fn [msg] (string msg)))) - (each nm ["InterruptedException" "java.lang.InterruptedException"] - (register-class-ctor! nm (fn [msg] (string msg)))) - # Character class statics (ASCII — the engine is byte-based). - (register-class-statics! "Character" - @{"isUpperCase" (fn [ch] (and (>= (ch :ch) 65) (<= (ch :ch) 90))) - "isLowerCase" (fn [ch] (and (>= (ch :ch) 97) (<= (ch :ch) 122)))}) - # java.net.URI constructor: stores the spec string, rounds-trips through str. - (each nm ["URI" "java.net.URI"] - (register-class-ctor! nm (fn [spec] (string spec)))) - # java.util.Date: millis-valued instants; no-arg = now. - (defn- make-date [&opt ms] - @{:jolt/type :jolt/date :ms (or ms (math/floor (* 1000 (os/clock :realtime))))}) - (each nm ["Date" "java.util.Date"] - (register-class-ctor! nm make-date)) - (register-tagged-methods! :jolt/date - @{"getTime" (fn [self] (self :ms)) - "toString" (fn [self] (string (self :ms)))}) - # java.util.TimeZone: getTimeZone(id) -> a tz value. - (defn- make-tz [id] @{:jolt/type :jolt/tz :id (string id)}) - (register-class-statics! "TimeZone" - @{"getTimeZone" (fn [id] (make-tz id))}) - (register-class-statics! "java.util.TimeZone" - @{"getTimeZone" (fn [id] (make-tz id))}) - # java.text.SimpleDateFormat: minimal formatter supporting y M d H m s tokens. - # (pad2 is the module-level helper above.) - (defn- sdf-format [pattern ms utc?] - (def d (os/date (math/floor (/ ms 1000)) (not utc?))) - (def out @"") - (var i 0) - (while (< i (length pattern)) - (def c (pattern i)) - (def k (do (var j i) - (while (and (< j (length pattern)) (= (pattern j) c)) (++ j)) - (- j i))) - (cond - (= c (chr "y")) (do (buffer/push out (if (>= k 4) (string (d :year)) (pad2 (mod (d :year) 100)))) (+= i k)) - (= c (chr "M")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month))) (pad2 (+ 1 (d :month))))) (+= i k)) - (= c (chr "d")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day))))) (+= i k)) - (= c (chr "H")) (do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k)) - (= c (chr "m")) (do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k)) - (= c (chr "s")) (do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k)) - (do (buffer/push out (string/from-bytes c)) (++ i)))) - (string out)) - (defn- make-sdf [pattern] - @{:jolt/type :jolt/sdf :pattern pattern :utc true}) - (each nm ["SimpleDateFormat" "java.text.SimpleDateFormat"] - (register-class-ctor! nm make-sdf)) - (register-tagged-methods! :jolt/sdf - @{"setTimeZone" (fn [self tz] - (put self :utc (= "UTC" (get tz :id))) - self) - "format" (fn [self date] - (sdf-format (self :pattern) (date :ms) (self :utc)))}) - # Thread stub: getContextClassLoader returns a stub so migratus jar/create code - # that walks Thread/currentThread doesn't crash. - (register-tagged-methods! :jolt/thread - @{"getContextClassLoader" (fn [self] @{:jolt/type :jolt/classloader})}) - # ClassLoader degrade (jolt-hjw): there is no classpath, so getResource returns - # nil and migratus's find-migration-dir falls through to the filesystem branch - # (resources/). getSystemClassLoader yields the same stub. - (each nm ["ClassLoader" "java.lang.ClassLoader"] - (register-class-statics! nm @{"getSystemClassLoader" (fn [] @{:jolt/type :jolt/classloader})})) - # No STM on jolt: a transaction is never running, so logging libraries that - # gate agent-vs-direct on it (clojure.tools.logging/log*) always log directly. - (register-class-statics! "clojure.lang.LockingTransaction" @{"isRunning" (fn [] false)}) - (register-tagged-methods! :jolt/classloader - @{"getResource" (fn [self path] nil) - "getResources" (fn [self path] nil) - "getResourceAsStream" (fn [self path] nil)}) - # next.jdbc host shims (paired with the __jdbc-* builtins in core.janet and the - # instance? Connection case in evaluator.janet). The wrapped connection carries - # a clj :exec callback (run one SQL string) and a :close callback. - # java.sql.Timestamp: migratus builds (Timestamp. millis) for the applied - # column; represent it as the millis number so it stores and sorts directly. - (each nm ["Timestamp" "java.sql.Timestamp"] - (register-class-ctor! nm (fn [ms] ms))) - (register-tagged-methods! :jolt/jdbc-conn - @{"setAutoCommit" (fn [self _] self) - "isClosed" (fn [self] (get (get self :closed) 0)) - "close" (fn [self] - (unless (get (get self :closed) 0) - ((get self :close)) - (put (get self :closed) 0 true)) - nil) - "getMetaData" (fn [self] @{:jolt/type :jolt/jdbc-meta :product (get self :product)})}) - (register-tagged-methods! :jolt/jdbc-meta - @{"getDatabaseProductName" (fn [self] (get self :product))}) - # Statement batch: addBatch accumulates SQL strings, executeBatch runs each via - # the connection's clj :exec callback (which executes inside the transaction). - (register-tagged-methods! :jolt/jdbc-stmt - @{"addBatch" (fn [self sql] (array/push (get self :cmds) sql) nil) - "executeBatch" (fn [self] - (def out @[]) - (each c (get self :cmds) (array/push out ((get self :exec) c))) - out) - "close" (fn [self] nil)}) - # java.io.File model (jolt-hjw). A :jolt/file carries its path; io/file and the - # File. ctor build it (see core.janet's __make-file). The method surface below - # is backed by os/ and file/. listFiles returns child :jolt/file values so - # file-seq (File-aware) yields :jolt/file leaves migratus can call methods on. - (defn- jfile-path [x] - (if (and (table? x) (= :jolt/file (get x :jolt/type))) (get x :path) (string x))) - (defn- make-jfile [path &opt child] - @{:jolt/type :jolt/file - :path (if child (string (jfile-path path) "/" (jfile-path child)) (jfile-path path))}) - (defn- last-slash [p] - (var idx nil) (var i 0) - (while (< i (length p)) (when (= (p i) 47) (set idx i)) (++ i)) - idx) - (defn- jfile-name [p] - (if-let [i (last-slash p)] (string/slice p (+ i 1)) p)) - (defn- jfile-abs [p] - (if (string/has-prefix? "/" p) p (string (os/cwd) "/" p))) - (each nm ["File" "java.io.File"] - (register-class-ctor! nm make-jfile)) - (register-tagged-methods! :jolt/file - @{"getPath" (fn [self] (get self :path)) - "toString" (fn [self] (get self :path)) - "getName" (fn [self] (jfile-name (get self :path))) - "getParent" (fn [self] (let [p (get self :path)] - (if-let [i (last-slash p)] (string/slice p 0 i) nil))) - "getAbsolutePath" (fn [self] (jfile-abs (get self :path))) - "getCanonicalPath" (fn [self] (jfile-abs (get self :path))) - "getAbsoluteFile" (fn [self] (make-jfile (jfile-abs (get self :path)))) - "exists" (fn [self] (not (nil? (os/stat (get self :path))))) - "isFile" (fn [self] (= :file (os/stat (get self :path) :mode))) - "isDirectory" (fn [self] (= :directory (os/stat (get self :path) :mode))) - "canRead" (fn [self] (not (nil? (os/stat (get self :path))))) - # listFiles: child File values, or nil when not a directory (Clojure null) - "listFiles" (fn [self] - (let [p (get self :path)] - (when (= :directory (os/stat p :mode)) - (map (fn [e] (make-jfile p e)) (os/dir p))))) - "list" (fn [self] - (let [p (get self :path)] - (when (= :directory (os/stat p :mode)) (os/dir p)))) - "toPath" (fn [self] @{:jolt/type :jolt/nio-path :s (get self :path)}) - "toURI" (fn [self] (string "file:" (jfile-abs (get self :path)))) - "toURL" (fn [self] @{:jolt/type :jolt/url :spec (string "file:" (jfile-abs (get self :path)))}) - "delete" (fn [self] (let [r (protect (os/rm (get self :path)))] (truthy? (r 0)))) - "mkdir" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0))) - "mkdirs" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0))) - "createNewFile" (fn [self] - (let [p (get self :path)] - (if (os/stat p) false - (do (def f (file/open p :w)) (file/close f) true)))) - "equals" (fn [self o] (and (table? o) (= (get self :path) (get o :path)))) - "hashCode" (fn [self] (hash (get self :path)))}) - # java.nio.file degrade for migratus's script-excluded? glob check: just enough - # of Path / FileSystem / PathMatcher to match a filename against a glob, with a - # simple recursive * / ? matcher (no path-segment semantics — filenames only). - (defn- glob-matches? [glob s] - (defn m [gi si] - (cond - (= gi (length glob)) (= si (length s)) - (= (glob gi) 42) # * - (or (m (+ gi 1) si) - (and (< si (length s)) (m gi (+ si 1)))) - (and (< si (length s)) - (or (= (glob gi) 63) (= (glob gi) (s si)))) # ? or literal - (m (+ gi 1) (+ si 1)) - false)) - (m 0 0)) - (register-tagged-methods! :jolt/nio-path - @{"getFileSystem" (fn [self] @{:jolt/type :jolt/nio-fs}) - "toString" (fn [self] (get self :s))}) - (register-tagged-methods! :jolt/nio-fs - @{"getPath" (fn [self s & _] @{:jolt/type :jolt/nio-path :s s}) - "getPathMatcher" (fn [self spec] - @{:jolt/type :jolt/nio-matcher - :glob (if (string/has-prefix? "glob:" spec) - (string/slice spec 5) spec)})}) - (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) (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))))) +# The JVM class/method shim surface that portable cljc libraries call now lives in +# src/jolt/interop/ split by area: java_base (java.lang statics + java.time + +# shared coercion helpers), host_io (java.io/util/net/sql/text), collections +# (the late-bound .iterator/.nth/... hooks over jolt values). The registry +# MACHINERY (class-statics / tagged-methods / register-*!) still lives in the +# evaluator, which loads before this module — so the registries exist before any +# install! runs here. +# +# api imports this module (import ./host_interop) to trigger registration at load. +# Adding a JDK-area shim is now a one-file change under interop/ plus a line here. +(use ./interop/java_base) +(use ./interop/host_io) +(use ./interop/collections) (install!) (install-io!) diff --git a/src/jolt/interop/collections.janet b/src/jolt/interop/collections.janet new file mode 100644 index 0000000..665d6a6 --- /dev/null +++ b/src/jolt/interop/collections.janet @@ -0,0 +1,49 @@ +# Host interop — collection interop: wires the evaluator's late-bound hooks to +# core's collection ops so Java-style interop (.iterator/.nth/.count/.seq/...) +# works over jolt values. Split from host_interop.janet (jolt-jx5l, phase 1). +# Late-bound because the evaluator loads before core. +(use ../evaluator) +(use ../core) +(use ../types) +(use ../lazyseq) +(use ../pv) +(use ../plist) +(import ../phm) + +# 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) (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))))) diff --git a/src/jolt/interop/host_io.janet b/src/jolt/interop/host_io.janet new file mode 100644 index 0000000..4dc689b --- /dev/null +++ b/src/jolt/interop/host_io.janet @@ -0,0 +1,531 @@ +# Host interop — java.io / java.util / java.net / java.sql / java.text shims that +# carry portable cljc libraries (Selmer's char-by-char reader, ring-codec, +# migratus, yogthos/config). Split from host_interop.janet (jolt-jx5l, phase 1). +# install-io! registers them; depends on java_base for the shared chr/pad2 +# coercion helpers and the java.time `formatter` (SimpleDateFormat et al.). +(use ../evaluator) +(use ../regex) +(use ../core) +(use ../pv) +(use ../plist) +(use ../types) +(use ../lazyseq) +(import ../phm) +(use ./java_base) + +# --- java.io / java.lang shims (Selmer's template reader) --------------------- + +(defn- string-reader [src] + # :close makes with-open's __close happy (it calls (get x :close) when + # present); :read-line-fn matches the 50-io reader convention so line-seq + # works over readers io/reader hands back + (def self @{:jolt/type :jolt/jio-string-reader :s (string src) :pos 0 + :close (fn [] nil)}) + (put self :read-line-fn + (fn [] + (def {:s s :pos pos} self) + (when (< pos (length s)) + (def i (string/find "\n" s pos)) + (if (nil? i) + (do (put self :pos (length s)) (string/slice s pos)) + (do (put self :pos (+ i 1)) (string/slice s pos i)))))) + self) +(defn- string-builder [&opt init] + # a numeric arg is a CAPACITY (java.lang.StringBuilder(int)), not content + @{:jolt/type :jolt/string-builder + :buf (cond (nil? init) @"" (number? init) (buffer/new init) (buffer init))}) + +(defn make-string-writer [] + # :close lets with-open close the writer (core-close-resource calls :close); + # it's a no-op so .toString after with-open still sees the buffer. + @{:jolt/type :jolt/writer :buf @"" :sink nil :close (fn [] nil)}) +(defn make-out-writer [] + @{:jolt/type :jolt/writer :buf nil :sink prin}) + +(defn- render-piece [x] + (cond + (nil? x) "null" + (and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch)) + (string x))) + +# Writer.write(int) writes the CHAR for that code (unlike StringBuilder.append(int), +# which appends the int's digits). jolt chars are bytes, so this round-trips UTF-8 +# byte-for-byte with readLine. +(defn- writer-piece [x] + (if (number? x) (string/from-bytes (math/trunc x)) (render-piece x))) + +# Read one unit from any reader-ish value: our shims dispatch through their +# tagged "read"; a janet core/file reads one byte. -1 at EOF. +(defn- read-unit [r] + (cond + (and (or (table? r) (struct? r)) (get r :jolt/type)) + (((get tagged-methods (r :jolt/type)) "read") r) + (= :core/file (type r)) + (let [b (file/read r 1)] (if (or (nil? b) (= 0 (length b))) -1 (b 0))) + (error (string "not a reader: " (type r))))) + +(defn- pushback-reader [rdr] + # java.io.PushbackReader: read delegates to the wrapped reader unless + # something was unread; unread takes a char (or char code) and pushes it back + (def self @{:jolt/type :jolt/pushback-reader :rdr rdr :pushed @[] + :close (fn [] nil)}) + self) + +(defn install-io! [] + (register-tagged-methods! :jolt/pushback-reader + @{"read" (fn [self] + (if (> (length (self :pushed)) 0) + (array/pop (self :pushed)) + (read-unit (self :rdr)))) + "unread" (fn [self ch] + (array/push (self :pushed) + (if (number? ch) ch (get ch :ch))) + nil) + "close" (fn [self] nil)}) + (register-tagged-methods! :jolt/jio-string-reader + @{"read" (fn [self] + (if (>= (self :pos) (length (self :s))) + -1 + (let [b ((self :s) (self :pos))] + (put self :pos (+ 1 (self :pos))) + b))) + "readLine" (fn [self] ((self :read-line-fn))) + "mark" (fn [self &opt limit] (put self :marked (self :pos)) nil) + "reset" (fn [self] (put self :pos (or (self :marked) 0)) nil) + "skip" (fn [self n] (put self :pos (min (length (self :s)) (+ (self :pos) n))) n) + "close" (fn [self] nil)}) + # java.io.Writer / StringWriter — the print-method protocol surface + # (jolt-g1r). A writer either pushes to a sink fn (stdout/custom) or + # accumulates in a buffer (StringWriter). write/append coerce chars the + # same way StringBuilder does. + (register-tagged-methods! :jolt/writer + @{"write" (fn [self x] + (if (self :sink) + ((self :sink) (writer-piece x)) + (buffer/push-string (self :buf) (writer-piece x))) + nil) + "append" (fn [self x] + (if (self :sink) + ((self :sink) (render-piece x)) + (buffer/push-string (self :buf) (render-piece x))) + self) + "flush" (fn [self] nil) + "close" (fn [self] nil) + "toString" (fn [self] (string (or (self :buf) "")))}) + (register-tagged-methods! :jolt/string-builder + @{"append" (fn [self x] (buffer/push (self :buf) (render-piece x)) self) + "toString" (fn [self] (string (self :buf))) + "length" (fn [self] (length (self :buf))) + "setLength" (fn [self n] + (def buf (self :buf)) + (if (< n (length buf)) + (buffer/popn buf (- (length buf) n)) + (while (< (length buf) n) (buffer/push buf "\0"))) + nil) + "charAt" (fn [self i] {:jolt/type :jolt/char :ch ((self :buf) i)})}) + (each nm ["File" "java.io.File"] + (register-class-statics! nm @{"separator" "/" "separatorChar" {:jolt/type :jolt/char :ch 47}})) + (register-class-statics! "Boolean" + @{"parseBoolean" (fn [s] (= "true" (string/ascii-lower (string s)))) + "TRUE" true "FALSE" false}) + (register-class-statics! "Class" + @{"forName" (fn [nm] @{:jolt/type :jolt/class :name nm})}) + (each nm ["StringReader" "java.io.StringReader"] + (register-class-ctor! nm string-reader)) + (each nm ["StringBuilder" "java.lang.StringBuilder"] + (register-class-ctor! nm string-builder)) + (each nm ["StringWriter" "java.io.StringWriter"] + (register-class-ctor! nm make-string-writer)) + # --- java.net / java.util surface for ring-codec (ring-core enablement) --- + # URLEncoder/URLDecoder: www-form-urlencoded (space <-> +, %XX bytes, + # [A-Za-z0-9.*_-] literal). Charset args are accepted and ignored beyond + # the name (everything is UTF-8 bytes here). + (defn- url-unreserved? [b] + (or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90)) + (and (>= b 97) (<= b 122)) (= b 46) (= b 42) (= b 95) (= b 45))) + (defn- url-encode-www [s & _] + (def out @"") + (each b (string/bytes (string s)) + (cond + (url-unreserved? b) (buffer/push-byte out b) + (= b 32) (buffer/push-string out "+") + (buffer/push-string out (string/format "%%%02X" b)))) + (string out)) + (defn- hexv [b] + (cond (and (>= b 48) (<= b 57)) (- b 48) + (and (>= b 65) (<= b 70)) (- b 55) + (and (>= b 97) (<= b 102)) (- b 87) + (error "URLDecoder: malformed escape"))) + (defn- url-decode-www [s & _] + (def bytes (string/bytes (string s))) + (def n (length bytes)) + (def out @"") + (var i 0) + (while (< i n) + (def b (in bytes i)) + (cond + (= b 43) (do (buffer/push-string out " ") (++ i)) + (= b 37) (if (< (+ i 2) n) + (do (buffer/push-byte out (+ (* 16 (hexv (in bytes (+ i 1)))) (hexv (in bytes (+ i 2))))) + (+= i 3)) + (error "URLDecoder: incomplete escape")) + (do (buffer/push-byte out b) (++ i)))) + (string out)) + (each nm ["URLEncoder" "java.net.URLEncoder"] + (register-class-statics! nm @{"encode" url-encode-www})) + (each nm ["URLDecoder" "java.net.URLDecoder"] + (register-class-statics! nm @{"decode" url-decode-www})) + (each nm ["Charset" "java.nio.charset.Charset"] + (register-class-statics! nm @{"forName" (fn [nm*] @{:jolt/type :jolt/charset :name nm*})})) + # Base64 (RFC 4648): encoder/decoder singletons with encode/decode methods. + (def- b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + (defn- b64-encode [bs] + (def bytes (if (bytes? bs) bs (string bs))) + (def n (length bytes)) + (def out @"") + (var i 0) + (while (< i n) + (def b0 (in bytes i)) + (def b1 (if (< (+ i 1) n) (in bytes (+ i 1)))) + (def b2 (if (< (+ i 2) n) (in bytes (+ i 2)))) + (buffer/push-byte out (in b64-alphabet (brshift b0 2))) + (buffer/push-byte out (in b64-alphabet (bor (blshift (band b0 3) 4) (brshift (or b1 0) 4)))) + (buffer/push-string out (if (nil? b1) "=" (string/from-bytes (in b64-alphabet (bor (blshift (band b1 0xf) 2) (brshift (or b2 0) 6)))))) + (buffer/push-string out (if (nil? b2) "=" (string/from-bytes (in b64-alphabet (band b2 0x3f))))) + (+= i 3)) + (string out)) + (def- b64-rev (do (def t @{}) (eachp [i c] b64-alphabet (put t c i)) t)) + (defn- b64-decode [s] + (def cleaned (string/replace-all "=" "" (string s))) + (def out @"") + (var acc 0) (var bits 0) + (each c (string/bytes cleaned) + (def v (get b64-rev c)) + (when (nil? v) (error "Base64: illegal character")) + (set acc (bor (blshift acc 6) v)) + (+= bits 6) + (when (>= bits 8) + (-= bits 8) + (buffer/push-byte out (band (brshift acc bits) 0xff)))) + out) + (register-tagged-methods! :jolt/base64-encoder @{"encode" (fn [self bs] (b64-encode bs)) "encodeToString" (fn [self bs] (b64-encode bs))}) + (register-tagged-methods! :jolt/base64-decoder @{"decode" (fn [self s] (b64-decode s))}) + (each nm ["Base64" "java.util.Base64"] + (register-class-statics! nm + @{"getEncoder" (fn [] @{:jolt/type :jolt/base64-encoder}) + "getDecoder" (fn [] @{:jolt/type :jolt/base64-decoder})})) + # Integer statics: valueOf with optional radix. Returns a plain number — + # byteValue/intValue live on the number method surface in the evaluator. + (register-class-statics! "Integer" + @{"valueOf" (fn [x &opt radix] + (cond + (number? x) x + (nil? radix) (or (scan-number (string x)) (error (string "NumberFormatException: " x))) + (= radix 16) (or (scan-number (string "16r" x)) (error (string "NumberFormatException: " x))) + (= radix 8) (or (scan-number (string "8r" x)) (error (string "NumberFormatException: " x))) + (= radix 2) (or (scan-number (string "2r" x)) (error (string "NumberFormatException: " x))) + (error (string "Integer/valueOf: unsupported radix " radix)))) + "parseInt" (fn [x &opt radix] + (or (scan-number (string (case radix 16 "16r" 8 "8r" 2 "2r" "") x)) + (error (string "NumberFormatException: " x)))) + "MAX_VALUE" 2147483647 + "MIN_VALUE" -2147483648}) + # StringTokenizer: eager split on any delimiter char, empty tokens skipped. + (defn- tokenize [s delims] + (def dset @{}) + (each b (string/bytes delims) (put dset b true)) + (def toks @[]) + (def cur @"") + (each b (string/bytes (string s)) + (if (get dset b) + (when (> (length cur) 0) (array/push toks (string cur)) (buffer/clear cur)) + (buffer/push-byte cur b))) + (when (> (length cur) 0) (array/push toks (string cur))) + toks) + (register-tagged-methods! :jolt/string-tokenizer + @{"hasMoreTokens" (fn [self] (< (self :pos) (length (self :toks)))) + "countTokens" (fn [self] (- (length (self :toks)) (self :pos))) + "nextToken" (fn [self] + (if (< (self :pos) (length (self :toks))) + (let [t (in (self :toks) (self :pos))] + (put self :pos (+ 1 (self :pos))) t) + (error "NoSuchElementException")))}) + (each nm ["StringTokenizer" "java.util.StringTokenizer"] + (register-class-ctor! nm (fn [s &opt delims] + @{:jolt/type :jolt/string-tokenizer + :toks (tokenize s (or delims " \t\n\r\f")) + :pos 0}))) + # clojure.lang.MapEntry: a 2-tuple, jolt's map-entry representation. + # java.util.HashMap: a mutable wrapper over a janet table, keyed by canonical + # key (so jolt collection keys compare by value). reitit uses it as a fast + # read cache: (HashMap. m) copies a map's entries, (.get hm k) reads. + # raw value-keys: reitit's HashMap keys are strings/keywords/tuples, all of + # which janet tables key by value — no canonicalization needed here. + (defn- hm-entries [m] + (cond (phm/phm? m) (phm/phm-entries m) + (struct? m) (pairs m) + (table? m) (pairs m) + @[])) + (register-tagged-methods! :jolt/hashmap + @{"get" (fn [self k] (get (self :tbl) k)) + "put" (fn [self k v] (put (self :tbl) k v) v) + "putAll" (fn [self m] (each pair (hm-entries m) (put (self :tbl) (in pair 0) (in pair 1))) nil) + "containsKey" (fn [self k] (not (nil? (get (self :tbl) k)))) + "size" (fn [self] (length (self :tbl)))}) + (each nm ["HashMap" "java.util.HashMap"] + # java.util.HashMap constructors: (), (initialCapacity), (initialCapacity, + # loadFactor) all start empty; (Map m) copies entries. A numeric first arg is + # a capacity (sizing hint we ignore), not content. + (register-class-ctor! nm + (fn [& args] + (def tbl @{}) + (def init (if (> (length args) 0) (in args 0) nil)) + (when (and init (not (number? init))) + (each pair (hm-entries init) (put tbl (in pair 0) (in pair 1)))) + @{:jolt/type :jolt/hashmap :tbl tbl}))) + (each nm ["MapEntry" "clojure.lang.MapEntry"] + (register-class-ctor! nm (fn [k v] [k v]))) + # (String. bytes) / (String. bytes charset): UTF-8 bytes to string. + (each nm ["String" "java.lang.String"] + (register-class-ctor! nm (fn [x &opt charset] (string x)))) + # java.net.URL: enough for selmer's template cache — file: URLs only. + # A protocol-less spec throws (selmer catches MalformedURLException and + # prepends file:///), and getPath hands back a stat-able filesystem path. + (defn url-path [spec] + (var p (if (string/has-prefix? "file:" spec) (string/slice spec 5) spec)) + (while (and (> (length p) 1) (string/has-prefix? "//" p)) + (set p (string/slice p 1))) + p) + (register-tagged-methods! :jolt/url + @{"getPath" (fn [self] (url-path (self :spec))) + "getFile" (fn [self] (url-path (self :spec))) + "toString" (fn [self] (self :spec)) + "toExternalForm" (fn [self] (self :spec))}) + (each nm ["URL" "java.net.URL"] + (register-class-ctor! nm + (fn [spec & _] + (def s (string spec)) + (def colon (string/find ":" s)) + (if (or (nil? colon) (= colon 0) + (string/find "/" (string/slice s 0 colon))) + (error (string "MalformedURLException: no protocol: " s)) + @{:jolt/type :jolt/url :spec s})))) + (each nm ["PushbackReader" "java.io.PushbackReader"] + (register-class-ctor! nm (fn [rdr &opt size] (pushback-reader rdr)))) + (each nm ["BigInteger" "java.math.BigInteger"] + (register-class-ctor! nm + (fn [v] + (or (scan-number (string/trim (string v))) + (error (string "NumberFormatException: For input string: \"" v "\"")))))) + (each nm ["Locale" "java.util.Locale"] + (register-class-ctor! nm (fn [id &opt _country] @{:jolt/type :jolt/locale :id (string id)}))) + # java.util.regex.Pattern statics: Pattern/compile, Pattern/quote, Pattern/MULTILINE. + # Pattern/compile returns jolt's native :jolt/regex compiled value so that + # str/replace, re-matches, .split etc accept it transparently. + (defn- pattern-quote [s] + (def meta "\\.[]{}()*+-?^$|&") + (def buf @"") + (var i 0) + (while (< i (length s)) + (def c (s i)) + (if (string/find (string/from-bytes c) meta) + (buffer/push buf (chr "\\"))) + (buffer/push buf (string/from-bytes c)) + (++ i)) + (string buf)) + (def pattern-multiline 8) + (each nm ["Pattern" "java.util.regex.Pattern"] + (register-class-statics! nm + @{"compile" (fn [s &opt flags] + (if (and flags (= (band flags pattern-multiline) pattern-multiline)) + (re-pattern (string "(?m)" s)) + (re-pattern s))) + "quote" (fn [s] (pattern-quote s)) + "MULTILINE" pattern-multiline})) + # .split on compiled regex values: delegates to re-split, drops trailing empties + (register-tagged-methods! :jolt/regex + @{"split" (fn [self s &opt limit] + (def parts (re-split self s)) + (while (and (> (length parts) 0) (= "" (last parts))) + (array/pop parts)) + parts)}) + # JVM exception constructors: (Exception. msg), (IllegalArgumentException. msg), + # (InterruptedException. msg). Return the message string so getMessage works. + (each nm ["Exception" "java.lang.Exception"] + (register-class-ctor! nm (fn [msg] (string msg)))) + (each nm ["IllegalArgumentException" "java.lang.IllegalArgumentException"] + (register-class-ctor! nm (fn [msg] (string msg)))) + (each nm ["InterruptedException" "java.lang.InterruptedException"] + (register-class-ctor! nm (fn [msg] (string msg)))) + # Character class statics (ASCII — the engine is byte-based). + (register-class-statics! "Character" + @{"isUpperCase" (fn [ch] (and (>= (ch :ch) 65) (<= (ch :ch) 90))) + "isLowerCase" (fn [ch] (and (>= (ch :ch) 97) (<= (ch :ch) 122)))}) + # java.net.URI constructor: stores the spec string, rounds-trips through str. + (each nm ["URI" "java.net.URI"] + (register-class-ctor! nm (fn [spec] (string spec)))) + # java.util.Date: millis-valued instants; no-arg = now. + (defn- make-date [&opt ms] + @{:jolt/type :jolt/date :ms (or ms (math/floor (* 1000 (os/clock :realtime))))}) + (each nm ["Date" "java.util.Date"] + (register-class-ctor! nm make-date)) + (register-tagged-methods! :jolt/date + @{"getTime" (fn [self] (self :ms)) + "toString" (fn [self] (string (self :ms)))}) + # java.util.TimeZone: getTimeZone(id) -> a tz value. + (defn- make-tz [id] @{:jolt/type :jolt/tz :id (string id)}) + (register-class-statics! "TimeZone" + @{"getTimeZone" (fn [id] (make-tz id))}) + (register-class-statics! "java.util.TimeZone" + @{"getTimeZone" (fn [id] (make-tz id))}) + # java.text.SimpleDateFormat: minimal formatter supporting y M d H m s tokens. + # (pad2 is the module-level helper above.) + (defn- sdf-format [pattern ms utc?] + (def d (os/date (math/floor (/ ms 1000)) (not utc?))) + (def out @"") + (var i 0) + (while (< i (length pattern)) + (def c (pattern i)) + (def k (do (var j i) + (while (and (< j (length pattern)) (= (pattern j) c)) (++ j)) + (- j i))) + (cond + (= c (chr "y")) (do (buffer/push out (if (>= k 4) (string (d :year)) (pad2 (mod (d :year) 100)))) (+= i k)) + (= c (chr "M")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month))) (pad2 (+ 1 (d :month))))) (+= i k)) + (= c (chr "d")) (do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day))))) (+= i k)) + (= c (chr "H")) (do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k)) + (= c (chr "m")) (do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k)) + (= c (chr "s")) (do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k)) + (do (buffer/push out (string/from-bytes c)) (++ i)))) + (string out)) + (defn- make-sdf [pattern] + @{:jolt/type :jolt/sdf :pattern pattern :utc true}) + (each nm ["SimpleDateFormat" "java.text.SimpleDateFormat"] + (register-class-ctor! nm make-sdf)) + (register-tagged-methods! :jolt/sdf + @{"setTimeZone" (fn [self tz] + (put self :utc (= "UTC" (get tz :id))) + self) + "format" (fn [self date] + (sdf-format (self :pattern) (date :ms) (self :utc)))}) + # Thread stub: getContextClassLoader returns a stub so migratus jar/create code + # that walks Thread/currentThread doesn't crash. + (register-tagged-methods! :jolt/thread + @{"getContextClassLoader" (fn [self] @{:jolt/type :jolt/classloader})}) + # ClassLoader degrade (jolt-hjw): there is no classpath, so getResource returns + # nil and migratus's find-migration-dir falls through to the filesystem branch + # (resources/). getSystemClassLoader yields the same stub. + (each nm ["ClassLoader" "java.lang.ClassLoader"] + (register-class-statics! nm @{"getSystemClassLoader" (fn [] @{:jolt/type :jolt/classloader})})) + # No STM on jolt: a transaction is never running, so logging libraries that + # gate agent-vs-direct on it (clojure.tools.logging/log*) always log directly. + (register-class-statics! "clojure.lang.LockingTransaction" @{"isRunning" (fn [] false)}) + (register-tagged-methods! :jolt/classloader + @{"getResource" (fn [self path] nil) + "getResources" (fn [self path] nil) + "getResourceAsStream" (fn [self path] nil)}) + # next.jdbc host shims (paired with the __jdbc-* builtins in core.janet and the + # instance? Connection case in evaluator.janet). The wrapped connection carries + # a clj :exec callback (run one SQL string) and a :close callback. + # java.sql.Timestamp: migratus builds (Timestamp. millis) for the applied + # column; represent it as the millis number so it stores and sorts directly. + (each nm ["Timestamp" "java.sql.Timestamp"] + (register-class-ctor! nm (fn [ms] ms))) + (register-tagged-methods! :jolt/jdbc-conn + @{"setAutoCommit" (fn [self _] self) + "isClosed" (fn [self] (get (get self :closed) 0)) + "close" (fn [self] + (unless (get (get self :closed) 0) + ((get self :close)) + (put (get self :closed) 0 true)) + nil) + "getMetaData" (fn [self] @{:jolt/type :jolt/jdbc-meta :product (get self :product)})}) + (register-tagged-methods! :jolt/jdbc-meta + @{"getDatabaseProductName" (fn [self] (get self :product))}) + # Statement batch: addBatch accumulates SQL strings, executeBatch runs each via + # the connection's clj :exec callback (which executes inside the transaction). + (register-tagged-methods! :jolt/jdbc-stmt + @{"addBatch" (fn [self sql] (array/push (get self :cmds) sql) nil) + "executeBatch" (fn [self] + (def out @[]) + (each c (get self :cmds) (array/push out ((get self :exec) c))) + out) + "close" (fn [self] nil)}) + # java.io.File model (jolt-hjw). A :jolt/file carries its path; io/file and the + # File. ctor build it (see core.janet's __make-file). The method surface below + # is backed by os/ and file/. listFiles returns child :jolt/file values so + # file-seq (File-aware) yields :jolt/file leaves migratus can call methods on. + (defn- jfile-path [x] + (if (and (table? x) (= :jolt/file (get x :jolt/type))) (get x :path) (string x))) + (defn- make-jfile [path &opt child] + @{:jolt/type :jolt/file + :path (if child (string (jfile-path path) "/" (jfile-path child)) (jfile-path path))}) + (defn- last-slash [p] + (var idx nil) (var i 0) + (while (< i (length p)) (when (= (p i) 47) (set idx i)) (++ i)) + idx) + (defn- jfile-name [p] + (if-let [i (last-slash p)] (string/slice p (+ i 1)) p)) + (defn- jfile-abs [p] + (if (string/has-prefix? "/" p) p (string (os/cwd) "/" p))) + (each nm ["File" "java.io.File"] + (register-class-ctor! nm make-jfile)) + (register-tagged-methods! :jolt/file + @{"getPath" (fn [self] (get self :path)) + "toString" (fn [self] (get self :path)) + "getName" (fn [self] (jfile-name (get self :path))) + "getParent" (fn [self] (let [p (get self :path)] + (if-let [i (last-slash p)] (string/slice p 0 i) nil))) + "getAbsolutePath" (fn [self] (jfile-abs (get self :path))) + "getCanonicalPath" (fn [self] (jfile-abs (get self :path))) + "getAbsoluteFile" (fn [self] (make-jfile (jfile-abs (get self :path)))) + "exists" (fn [self] (not (nil? (os/stat (get self :path))))) + "isFile" (fn [self] (= :file (os/stat (get self :path) :mode))) + "isDirectory" (fn [self] (= :directory (os/stat (get self :path) :mode))) + "canRead" (fn [self] (not (nil? (os/stat (get self :path))))) + # listFiles: child File values, or nil when not a directory (Clojure null) + "listFiles" (fn [self] + (let [p (get self :path)] + (when (= :directory (os/stat p :mode)) + (map (fn [e] (make-jfile p e)) (os/dir p))))) + "list" (fn [self] + (let [p (get self :path)] + (when (= :directory (os/stat p :mode)) (os/dir p)))) + "toPath" (fn [self] @{:jolt/type :jolt/nio-path :s (get self :path)}) + "toURI" (fn [self] (string "file:" (jfile-abs (get self :path)))) + "toURL" (fn [self] @{:jolt/type :jolt/url :spec (string "file:" (jfile-abs (get self :path)))}) + "delete" (fn [self] (let [r (protect (os/rm (get self :path)))] (truthy? (r 0)))) + "mkdir" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0))) + "mkdirs" (fn [self] (truthy? ((protect (os/mkdir (get self :path))) 0))) + "createNewFile" (fn [self] + (let [p (get self :path)] + (if (os/stat p) false + (do (def f (file/open p :w)) (file/close f) true)))) + "equals" (fn [self o] (and (table? o) (= (get self :path) (get o :path)))) + "hashCode" (fn [self] (hash (get self :path)))}) + # java.nio.file degrade for migratus's script-excluded? glob check: just enough + # of Path / FileSystem / PathMatcher to match a filename against a glob, with a + # simple recursive * / ? matcher (no path-segment semantics — filenames only). + (defn- glob-matches? [glob s] + (defn m [gi si] + (cond + (= gi (length glob)) (= si (length s)) + (= (glob gi) 42) # * + (or (m (+ gi 1) si) + (and (< si (length s)) (m gi (+ si 1)))) + (and (< si (length s)) + (or (= (glob gi) 63) (= (glob gi) (s si)))) # ? or literal + (m (+ gi 1) (+ si 1)) + false)) + (m 0 0)) + (register-tagged-methods! :jolt/nio-path + @{"getFileSystem" (fn [self] @{:jolt/type :jolt/nio-fs}) + "toString" (fn [self] (get self :s))}) + (register-tagged-methods! :jolt/nio-fs + @{"getPath" (fn [self s & _] @{:jolt/type :jolt/nio-path :s s}) + "getPathMatcher" (fn [self spec] + @{:jolt/type :jolt/nio-matcher + :glob (if (string/has-prefix? "glob:" spec) + (string/slice spec 5) spec)})}) + (register-tagged-methods! :jolt/nio-matcher + @{"matches" (fn [self path] (glob-matches? (get self :glob) (get path :s)))})) diff --git a/src/jolt/interop/java_base.janet b/src/jolt/interop/java_base.janet new file mode 100644 index 0000000..20fe1bb --- /dev/null +++ b/src/jolt/interop/java_base.janet @@ -0,0 +1,248 @@ +# Host interop — java.lang static surfaces (Math/Thread/System/Long) + the +# java.time shim (epoch-ms values + a DateTimeFormatter pattern subset). Split +# from host_interop.janet (jolt-jx5l, phase 1). Everything registers through the +# evaluator's class-statics / tagged-methods registries via install!. Also the +# home of the shared coercion helpers (chr/pad2) that host_io reuses. +(use ../evaluator) +(use ../regex) +(use ../core) +(use ../pv) +(use ../plist) +(use ../types) +(use ../lazyseq) +(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}) +(defn- zoned [ms zone] @{:jolt/type :jolt/zoned-dt :ms ms :zone zone}) +(defn- local-dt [ms] @{:jolt/type :jolt/local-dt :ms ms}) +(defn formatter [pattern &opt locale] @{:jolt/type :jolt/dt-formatter :pattern pattern :locale locale}) + +(def- zone-default @{:jolt/type :jolt/zone-id :id "system"}) + +# ms of any date-ish shim value (or a :jolt/inst) +(defn- ms-of [d] + (cond + (number? d) d + (and (or (table? d) (struct? d)) + (or (= :jolt/inst (get d :jolt/type)) + (= :jolt/instant (get d :jolt/type)) + (= :jolt/zoned-dt (get d :jolt/type)) + (= :jolt/local-dt (get d :jolt/type)))) + (get d :ms) + (error (string "not a date value: " (type d))))) + +# --- formatting ---------------------------------------------------------------- + +(def- month-names ["January" "February" "March" "April" "May" "June" "July" + "August" "September" "October" "November" "December"]) +(def- day-names ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"]) + +(defn pad2 [n] (if (< n 10) (string "0" n) (string n))) + +# Format epoch-ms with a (subset of the) JVM DateTimeFormatter pattern: +# yyyy yy MMMM MMM MM M dd d EEEE EEE HH H hh h mm m ss s a, quoted literals +# with '...'. Unknown letters pass through. +(defn- format-ms [pattern ms] + (def d (os/date (math/floor (/ ms 1000)) true)) + (def out @"") + (var i 0) + (def n (length pattern)) + (defn run-len [c] + (var j i) + (while (and (< j n) (= (pattern j) c)) (++ j)) + (- j i)) + (while (< i n) + (def c (pattern i)) + (def k (run-len c)) + (cond + (= c (chr "'")) + # quoted literal up to the closing quote ('' = literal quote) + (if (and (< (+ i 1) n) (= (pattern (+ i 1)) (chr "'"))) + (do (buffer/push out "'") (+= i 2)) + (let [close (string/find "'" pattern (+ i 1))] + (buffer/push out (string/slice pattern (+ i 1) close)) + (set i (+ close 1)))) + (= c (chr "y")) + (do (buffer/push out (if (>= k 4) (string (d :year)) + (pad2 (mod (d :year) 100)))) + (+= i k)) + (= c (chr "M")) + (do (buffer/push out (case k + 1 (string (+ 1 (d :month))) + 2 (pad2 (+ 1 (d :month))) + 3 (string/slice (in month-names (d :month)) 0 3) + (in month-names (d :month)))) + (+= i k)) + (= c (chr "d")) + (do (buffer/push out (if (= k 1) (string (+ 1 (d :month-day))) (pad2 (+ 1 (d :month-day))))) + (+= i k)) + (= c (chr "E")) + (do (buffer/push out (if (>= k 4) (in day-names (d :week-day)) + (string/slice (in day-names (d :week-day)) 0 3))) + (+= i k)) + (= c (chr "H")) + (do (buffer/push out (if (= k 1) (string (d :hours)) (pad2 (d :hours)))) (+= i k)) + (= c (chr "h")) + (let [h12 (let [h (mod (d :hours) 12)] (if (= h 0) 12 h))] + (buffer/push out (if (= k 1) (string h12) (pad2 h12))) (+= i k)) + (= c (chr "m")) + (do (buffer/push out (if (= k 1) (string (d :minutes)) (pad2 (d :minutes)))) (+= i k)) + (= c (chr "s")) + (do (buffer/push out (if (= k 1) (string (d :seconds)) (pad2 (d :seconds)))) (+= i k)) + (= c (chr "a")) + (do (buffer/push out (if (< (d :hours) 12) "AM" "PM")) (+= i k)) + (do (buffer/push out (string/from-bytes c)) (++ i)))) + (string out)) + +# Localized FormatStyle approximations (no locale database on this host). +(def- style-patterns + {[:date :short] "M/d/yy" [:date :medium] "MMM d, yyyy" + [:date :long] "MMMM d, yyyy" [:date :full] "EEEE, MMMM d, yyyy" + [:time :short] "h:mm a" [:time :medium] "h:mm:ss a" + [:time :long] "h:mm:ss a" [:time :full] "h:mm:ss a" + [:datetime :short] "M/d/yy, h:mm a" + [:datetime :medium] "MMM d, yyyy, h:mm:ss a" + [:datetime :long] "MMMM d, yyyy, h:mm:ss a" + [:datetime :full] "EEEE, MMMM d, yyyy, h:mm:ss a"}) + +(defn- style-fmt [kind style] + (formatter (get style-patterns [kind (get style :style)] "yyyy-MM-dd HH:mm:ss"))) + +# --- 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)}) + (register-class-statics! "DateTimeFormatter" + @{"ofPattern" (fn [p &opt locale] (formatter p locale)) + "ISO_LOCAL_DATE" (formatter "yyyy-MM-dd") + "ISO_LOCAL_DATE_TIME" (formatter "yyyy-MM-dd'T'HH:mm:ss") + "ofLocalizedDate" (fn [style] (style-fmt :date style)) + "ofLocalizedTime" (fn [style] (style-fmt :time style)) + "ofLocalizedDateTime" (fn [style] (style-fmt :datetime style))}) + (register-class-statics! "Instant" + @{"ofEpochMilli" (fn [ms] (instant ms)) + "now" (fn [] (instant (math/floor (* 1000 (os/clock :realtime)))))}) + (register-class-statics! "ZoneId" + @{"systemDefault" (fn [] zone-default)}) + (register-class-statics! "LocalDateTime" + @{"ofInstant" (fn [inst zone] (local-dt (ms-of inst))) + "now" (fn [] (local-dt (math/floor (* 1000 (os/clock :realtime)))))}) + (let [locale-statics @{"getDefault" (fn [] @{:jolt/type :jolt/locale :id "default"}) + "ENGLISH" @{:jolt/type :jolt/locale :id "en"} + "US" @{:jolt/type :jolt/locale :id "en-US"} + "ROOT" @{:jolt/type :jolt/locale :id "root"}}] + (each nm ["Locale" "java.util.Locale"] + (register-class-statics! nm locale-statics))) + (register-tagged-methods! :jolt/instant + @{"atZone" (fn [self zone] (zoned (self :ms) zone)) + "toEpochMilli" (fn [self] (self :ms))}) + (register-tagged-methods! :jolt/zoned-dt + @{"toLocalDateTime" (fn [self] (local-dt (self :ms))) + "toInstant" (fn [self] (instant (self :ms)))}) + (register-tagged-methods! :jolt/local-dt + @{"atZone" (fn [self zone] (zoned (self :ms) zone))}) + # a :jolt/inst (#inst — Clojure's java.util.Date) supports the Date methods + # Selmer's fix-date path calls + (register-tagged-methods! :jolt/inst + @{"toInstant" (fn [self] (instant (self :ms))) + "getTime" (fn [self] (self :ms))}) + (register-tagged-methods! :jolt/dt-formatter + @{"withLocale" (fn [self locale] (formatter (self :pattern) locale)) + "format" (fn [self d] (format-ms (self :pattern) (ms-of d)))}))