diff --git a/host/chez/emit.janet b/host/chez/emit.janet index 30e312a..30efec4 100644 --- a/host/chez/emit.janet +++ b/host/chez/emit.janet @@ -107,6 +107,20 @@ (var- gensym-n 0) (defn- fresh-label [prefix] (string prefix (++ gensym-n))) +# Emit a call `(ctor a0 a1 ...)` with the args evaluated LEFT-TO-RIGHT. Chez's +# procedure-argument evaluation order is unspecified (and in practice right-to- +# left), but Clojure/JVM evaluates collection-literal elements left-to-right, so a +# literal like [(read r) (read r)] over side-effecting reads must bind in source +# order. Bind each arg to a fresh temp in a let* (sequential) then construct. Only +# wraps when there are >= 2 args (0/1 have no ordering to preserve), keeping the +# common small-literal output compact (jolt-avt6). +(defn- emit-ordered [ctor arg-strs] + (if (< (length arg-strs) 2) + (string "(" ctor (if (empty? arg-strs) "" (string " " (string/join arg-strs " "))) ")") + (let [tmps (map (fn [_] (fresh-label "_o$")) arg-strs) + binds (string/join (map (fn [t a] (string "(" t " " a ")")) tmps arg-strs) " ")] + (string "(let* (" binds ") (" ctor " " (string/join tmps " ") "))")))) + # Most jolt names are already valid Scheme identifiers (inc, even?, +, ->str all # are — Scheme allows ! $ % & * + - . / : < = > ? @ ^ _ ~). The one that isn't is # `#`, which jolt auto-gensyms use as a suffix (e.g. p1__0000X4# from #(...) @@ -345,7 +359,10 @@ # Host interop methods with a Chez RT shim (rt.ss jolt-host-call). A `.method` # call on any other method is out of subset until shimmed — keep this in sync. -(def- supported-host-methods {"write" true "isDirectory" true "listFiles" true}) +# `.write` is NOT here: StringWriter (a jhost, host-static.ss) handles .write via +# record-method-dispatch; the old jolt-host-call "write" fast-path (display to a +# port) would mis-route a writer to `(display x jhost)`. Keep the File-op methods. +(def- supported-host-methods {"isDirectory" true "listFiles" true}) # jolt's comparison ops are vacuously true at arity 1 and DON'T inspect the arg # (so (< :kw) is true), but Scheme's < demands a number even there — special-case. @@ -381,6 +398,13 @@ (= kind :coll) (string "(jolt-get " (emit fnode) " " (first args) default ")") (and (stdlib-var? fnode) (not prelude-mode?)) (errorf "emit: unsupported stdlib fn `%s/%s` (no core on Chez yet)" (get fnode :ns) (get fnode :name)) + # static method call (Class/method arg*) -> (host-static-call "Class" + # "method" arg*). host-static.ss resolves the method from the class-statics + # registry and applies it (jolt-avt6). + (= :host-static (get fnode :op)) + (string "(host-static-call " (chez-str-lit (get fnode :class)) " " + (chez-str-lit (get fnode :member)) + (if (empty? args) "" (string " " (string/join args " "))) ")") (= :host (get fnode :op)) (errorf "emit: unsupported host call `%s` (no host interop on Chez yet)" (get fnode :name)) # a :local callee that isn't a known procedure (a let/param binding holding a @@ -438,6 +462,14 @@ (string "(var-deref " (chez-str-lit (get node :ns)) " " (chez-str-lit (get node :name)) ")"))) :host (errorf "emit: unsupported host ref `%s` (no host interop on Chez yet)" (get node :name)) + # value-position static ref (Class/member, e.g. Long/MAX_VALUE, System/exit + # passed as a value) -> the registered static value/procedure (host-static.ss). + :host-static (string "(host-static-ref " (chez-str-lit (get node :class)) " " + (chez-str-lit (get node :member)) ")") + # constructor (Class. args*) / (new Class args*) -> (host-new "Class" args*). + :host-new (string "(host-new " (chez-str-lit (get node :class)) + (let [args (map emit (vv (get node :args)))] + (if (empty? args) "" (string " " (string/join args " ")))) ")") # (var x) / #'x -> the var cell itself (the rt.ss var-cell, a first-class var # object). var?/var-get/deref/invoke/= operate on it (vars.ss). :the-var (string "(jolt-var " (chez-str-lit (get node :ns)) " " (chez-str-lit (get node :name)) ")") @@ -450,15 +482,16 @@ (if (empty? (vv (get node :statements))) "" " ") (emit (get node :ret)) ")") :invoke (emit-invoke node) - # collection literals -> rt constructors (collections.ss) - :vector (string "(jolt-vector " (string/join (map emit (vv (get node :items))) " ") ")") - :set (string "(jolt-hash-set " (string/join (map emit (vv (get node :items))) " ") ")") + # collection literals -> rt constructors (collections.ss). Elements evaluate + # LEFT-TO-RIGHT (emit-ordered) to match Clojure for side-effecting elements. + :vector (emit-ordered "jolt-vector" (map emit (vv (get node :items)))) + :set (emit-ordered "jolt-hash-set" (map emit (vv (get node :items)))) :map (let [flat @[]] (each p (vv (get node :pairs)) (def p (vv p)) (array/push flat (emit (get p 0))) (array/push flat (emit (get p 1)))) - (string "(jolt-hash-map " (string/join flat " ") ")")) + (emit-ordered "jolt-hash-map" flat)) :let (emit-let node) :loop (emit-loop node) :recur (emit-recur node) diff --git a/host/chez/host-static.ss b/host/chez/host-static.ss new file mode 100644 index 0000000..dc38e4b --- /dev/null +++ b/host/chez/host-static.ss @@ -0,0 +1,476 @@ +;; host-static.ss (jolt-avt6) — host class statics + constructors on Chez. +;; +;; The analyzer lowers `Class/member` to a :host-static node and `(Class. ...)` / +;; `(new Class ...)` to a :host-new node (jolt-core/jolt/analyzer.clj); the Chez +;; emit (emit.janet) lowers a value ref to (host-static-ref "Class" "member"), a +;; call head to (host-static-call "Class" "member" args...), and a constructor to +;; (host-new "Class" args...). This file is the runtime registry those three +;; resolve against — the Chez port of the seed's class-statics / class-ctors / +;; tagged-methods registries (src/jolt/interop/java_base.janet + host_io.janet), +;; restricted to the java.lang/util/net/io surface portable cljc code calls. +;; (java.time formatting is a separate increment.) +;; +;; Constructed host objects are `jhost` records (a tag + mutable state); their +;; (.method ...) calls reach record-method-dispatch (records.ss), extended below +;; with a jhost arm that dispatches through host-tagged-methods. +;; +;; Loaded from rt.ss LAST (after natives-str.ss / records.ss): it extends +;; record-method-dispatch and reuses jolt-str-render-one / jolt-re-pattern. + +;; ---- registries ------------------------------------------------------------- +(define class-statics-tbl (make-hashtable string-hash string=?)) ; "Class" -> (member-ht) +(define class-ctors-tbl (make-hashtable string-hash string=?)) ; "Class" -> ctor proc +(define host-methods-tbl (make-hashtable string-hash string=?)) ; tag -> (method-ht) + +;; A class token may arrive fully qualified (java.io.StringReader) or short +;; (StringReader). Register both; resolve by exact then by last dotted segment. +(define (short-class-name s) + (let loop ((i (- (string-length s) 1))) + (cond ((< i 0) s) + ((char=? (string-ref s i) #\.) (substring s (+ i 1) (string-length s))) + (else (loop (- i 1)))))) + +(define (register-class-statics! name members) ; members: list of (str . val/proc) + (let ((h (or (hashtable-ref class-statics-tbl name #f) + (let ((h (make-hashtable string-hash string=?))) + (hashtable-set! class-statics-tbl name h) h)))) + (for-each (lambda (p) (hashtable-set! h (car p) (cdr p))) members))) + +(define (register-class-ctor! name proc) (hashtable-set! class-ctors-tbl name proc)) + +(define (register-host-methods! tag members) + (let ((h (or (hashtable-ref host-methods-tbl tag #f) + (let ((h (make-hashtable string-hash string=?))) + (hashtable-set! host-methods-tbl tag h) h)))) + (for-each (lambda (p) (hashtable-set! h (car p) (cdr p))) members))) + +(define (lookup-class h-tbl name) + (or (hashtable-ref h-tbl name #f) + (hashtable-ref h-tbl (short-class-name name) #f))) + +;; ---- host object ------------------------------------------------------------ +(define-record-type jhost (fields tag (mutable state)) (nongenerative chez-jhost-v1)) + +;; record-method-dispatch (records.ss) gets a jhost arm: dispatch (.method obj a*) +;; through the tag's method table. +(define %hs-record-method-dispatch record-method-dispatch) +(set! record-method-dispatch + (lambda (obj method-name rest-args) + (cond + ((jhost? obj) + (let ((mh (hashtable-ref host-methods-tbl (jhost-tag obj) #f))) + (let ((f (and mh (hashtable-ref mh method-name #f)))) + (if f + (apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args))) + (error #f (string-append "No method " method-name " on host " (jhost-tag obj))))))) + ((number? obj) (number-method method-name obj)) + (else (%hs-record-method-dispatch obj method-name rest-args))))) + +;; java.lang.Number method surface (the boxed-number methods cljc code calls). The +;; integer projections wrap modulo their width (ring-codec relies on byteValue +;; overflow: (.byteValue 255) => -1); the float projections are identity flonums. +(define (number-method method n) + (cond + ((string=? method "byteValue") (let ((b (modulo (jnum->exact n) 256))) (->num (if (>= b 128) (- b 256) b)))) + ((string=? method "shortValue") (let ((b (modulo (jnum->exact n) 65536))) (->num (if (>= b 32768) (- b 65536) b)))) + ((string=? method "intValue") (->num (jnum->exact n))) + ((string=? method "longValue") (->num (jnum->exact n))) + ((string=? method "doubleValue") (->num n)) + ((string=? method "floatValue") (->num n)) + ((string=? method "toString") (jolt-num->string n)) + ((string=? method "hashCode") (->num (jnum->exact n))) + (else (error #f (string-append "No method " method " for number"))))) + +;; ---- emit entry points ------------------------------------------------------ +(define (host-static-ref class member) + (let ((h (lookup-class class-statics-tbl class))) + (if h + (let ((v (hashtable-ref h member #f))) + (if v v (error #f (string-append "No static " class "/" member)))) + (error #f (string-append "Unknown class " class))))) + +(define (host-static-call class member . args) + (apply (host-static-ref class member) args)) + +(define (host-new class . args) + (let ((ctor (lookup-class class-ctors-tbl class))) + (if ctor (apply ctor args) + (error #f (string-append "No constructor for class " class))))) + +;; ---- coercion helpers ------------------------------------------------------- +(define (->num x) (exact->inexact x)) +(define (jnum->exact n) (exact (truncate n))) +;; parse an integer string in radix; #f on failure (matches the seed's scan-number) +(define (parse-int-str s radix) + (let ((n (string->number (str-trim (if (string? s) s (jolt-str-render-one s))) radix))) + (and n (integer? n) (->num n)))) +(define (parse-int-or-throw s radix what) + (or (parse-int-str s radix) + (error #f (string-append "NumberFormatException: For input string: \"" + (if (string? s) s (jolt-str-render-one s)) "\"")))) +(define (char-code c) (if (char? c) (char->integer c) (jnum->exact c))) + +;; ---- java.lang statics ------------------------------------------------------ +(register-class-statics! "Math" + (list (cons "sqrt" (lambda (x) (->num (sqrt x)))) + (cons "pow" (lambda (a b) (->num (expt a b)))) + (cons "floor" (lambda (x) (->num (floor x)))) + (cons "ceil" (lambda (x) (->num (ceiling x)))) + (cons "round" (lambda (x) (->num (round x)))) + (cons "abs" (lambda (x) (->num (abs x)))) + (cons "sin" (lambda (x) (->num (sin x)))) (cons "cos" (lambda (x) (->num (cos x)))) + (cons "tan" (lambda (x) (->num (tan x)))) (cons "asin" (lambda (x) (->num (asin x)))) + (cons "acos" (lambda (x) (->num (acos x)))) (cons "atan" (lambda (x) (->num (atan x)))) + (cons "log" (lambda (x) (->num (log x)))) (cons "log10" (lambda (x) (->num (/ (log x) (log 10))))) + (cons "exp" (lambda (x) (->num (exp x)))) + (cons "max" (lambda (a b) (->num (if (> a b) a b)))) (cons "min" (lambda (a b) (->num (if (< a b) a b)))) + (cons "signum" (lambda (x) (cond ((< x 0) -1.0) ((> x 0) 1.0) (else 0.0)))) + (cons "PI" (->num (* 4 (atan 1)))) (cons "E" (->num (exp 1))) + (cons "random" (lambda args (->num (random 1.0)))))) + +;; Thread: no real threading here (futures/timing are jolt-byjr). sleep is a no-op. +(register-class-statics! "Thread" + (list (cons "sleep" (lambda (ms . _) jolt-nil)) + (cons "yield" (lambda () jolt-nil)) + (cons "interrupted" (lambda () #f)) + (cons "currentThread" (lambda () (make-jhost "thread" '()))))) +(register-host-methods! "thread" + (list (cons "getContextClassLoader" (lambda (self) (make-jhost "classloader" '()))))) + +(define (now-millis) + (let ((t (current-time 'time-utc))) + (+ (* 1000 (time-second t)) (quotient (time-nanosecond t) 1000000)))) +(register-class-statics! "System" + (list (cons "currentTimeMillis" (lambda () (->num (now-millis)))) + (cons "nanoTime" (lambda () (->num (* 1000000 (now-millis))))) + (cons "exit" (lambda args (exit (if (null? args) 0 (jnum->exact (car args)))))) + ;; wrapped in lambdas: the helpers are defined below, resolved at call time. + (cons "getProperty" (lambda (k . d) (apply sys-get-property k d))) + (cons "getProperties" (lambda () (sys-properties-map))) + (cons "getenv" (lambda k (apply sys-getenv k))))) + +(register-class-statics! "Long" + (list (cons "MAX_VALUE" (->num 9223372036854775807)) + (cons "MIN_VALUE" (->num -9223372036854775808)) + (cons "parseLong" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "parseLong"))) + (cons "valueOf" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "valueOf"))))) + +(register-class-statics! "Integer" + (list (cons "MAX_VALUE" (->num 2147483647)) (cons "MIN_VALUE" (->num -2147483648)) + (cons "valueOf" (lambda (x . r) + (if (number? x) (->num x) + (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "valueOf")))) + (cons "parseInt" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseInt"))))) + +(register-class-statics! "Boolean" + (list (cons "parseBoolean" (lambda (s) (string=? "true" (ascii-string-down (if (string? s) s (jolt-str-render-one s)))))) + (cons "TRUE" #t) (cons "FALSE" #f))) + +;; Character: ASCII predicates (the engine is byte/ASCII oriented). +(register-class-statics! "Character" + (list (cons "isUpperCase" (lambda (c) (let ((n (char-code c))) (and (>= n 65) (<= n 90))))) + (cons "isLowerCase" (lambda (c) (let ((n (char-code c))) (and (>= n 97) (<= n 122))))) + (cons "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57))))) + (cons "isWhitespace" (lambda (c) (char<=? (integer->char (char-code c)) #\space))))) + +;; String/valueOf(Object): "null" for nil, else jolt's str semantics. +(register-class-statics! "String" + (list (cons "valueOf" (lambda (x . _) (if (jolt-nil? x) "null" (jolt-str-render-one x)))))) + +(register-class-statics! "Class" + (list (cons "forName" (lambda (nm) (make-jhost "class" (list (cons 'name nm))))))) + +;; ---- System helpers (defined before use above via top-level order) ---------- +(define (sys-get-property k . dflt) + (cond ((string=? k "os.name") "Mac OS X") + ((string=? k "line.separator") "\n") + ((string=? k "file.separator") "/") + ((string=? k "path.separator") ":") + ((string=? k "user.dir") (or (getenv "PWD") ".")) + ((string=? k "user.home") (or (getenv "HOME") "")) + ((string=? k "java.io.tmpdir") (or (getenv "TMPDIR") "/tmp")) + ((pair? dflt) (car dflt)) + (else jolt-nil))) +(define (sys-properties-map) + (jolt-hash-map "os.name" "Mac OS X" "line.separator" "\n" "file.separator" "/" + "user.dir" (or (getenv "PWD") ".") "user.home" (or (getenv "HOME") "") + "java.io.tmpdir" (or (getenv "TMPDIR") "/tmp"))) + +;; full environment as an alist of (name . value), via spawning `env`. +(define (all-env-pairs) + (call-with-values + (lambda () (open-process-ports "env" (buffer-mode block) (native-transcoder))) + (lambda (stdin stdout stderr pid) + (let loop ((acc '())) + (let ((l (get-line stdout))) + (if (eof-object? l) (reverse acc) + (let ((eq (let scan ((i 0)) (cond ((= i (string-length l)) #f) + ((char=? (string-ref l i) #\=) i) + (else (scan (+ i 1))))))) + (loop (if eq (cons (cons (substring l 0 eq) (substring l (+ eq 1) (string-length l))) acc) acc))))))))) +;; JOLT_BAKE_ENV_ALLOWLIST (jolt-s3j): when set, only the listed comma-separated +;; names are served; unset (the normal case) reads are live and unfiltered. +(define (env-allowlist) + (let ((a (getenv "JOLT_BAKE_ENV_ALLOWLIST"))) + (and a (map str-trim (str-literal-split a ","))))) +(define (sys-getenv . k) + (let ((allow (env-allowlist))) + (if (null? k) + (apply jolt-hash-map + (let loop ((ps (all-env-pairs)) (acc '())) + (cond ((null? ps) (reverse acc)) + ((and allow (not (member (caar ps) allow))) (loop (cdr ps) acc)) + (else (loop (cdr ps) (cons (cdar ps) (cons (caar ps) acc))))))) + (let ((name (car k))) + (if (and allow (not (member name allow))) jolt-nil + (let ((v (getenv name))) (if v v jolt-nil))))))) + +;; ---- StringBuilder ---------------------------------------------------------- +;; state: a box (1-vector) holding the accumulated string. +(define (sb-str self) (vector-ref (jhost-state self) 0)) +(define (sb-set! self s) (vector-set! (jhost-state self) 0 s)) +(define (render-piece x) + (cond ((jolt-nil? x) "null") ((char? x) (string x)) ((string? x) x) + (else (jolt-str-render-one x)))) +(register-class-ctor! "StringBuilder" + (lambda args (make-jhost "string-builder" + ;; a numeric first arg is a CAPACITY hint, not content. + (vector (if (and (pair? args) (not (number? (car args)))) (render-piece (car args)) ""))))) +(register-host-methods! "string-builder" + (list (cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self)) + (cons "toString" (lambda (self) (sb-str self))) + (cons "length" (lambda (self) (->num (string-length (sb-str self))))) + (cons "charAt" (lambda (self i) (string-ref (sb-str self) (jnum->exact i)))) + (cons "setLength" (lambda (self n) + (let ((cur (sb-str self)) (n (jnum->exact n))) + (sb-set! self (if (< n (string-length cur)) + (substring cur 0 n) + (string-append cur (make-string (- n (string-length cur)) #\nul))))) + jolt-nil)))) + +;; ---- StringWriter ----------------------------------------------------------- +;; Writer.write(int) writes the CHAR for that code; append(char) appends the char. +(define (writer-piece x) (if (number? x) (string (integer->char (jnum->exact x))) (render-piece x))) +(register-class-ctor! "StringWriter" (lambda args (make-jhost "writer" (vector "")))) +(register-host-methods! "writer" + (list (cons "write" (lambda (self x) (sb-set! self (string-append (sb-str self) (writer-piece x))) jolt-nil)) + (cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self)) + (cons "flush" (lambda (self) jolt-nil)) + (cons "close" (lambda (self) jolt-nil)) + (cons "toString" (lambda (self) (sb-str self))))) + +;; ---- StringReader ----------------------------------------------------------- +;; state: a vector #(string pos marked). +(register-class-ctor! "StringReader" + (lambda (src . _) (make-jhost "string-reader" (vector (if (string? src) src (jolt-str-render-one src)) 0 0)))) +(define (sr-s self) (vector-ref (jhost-state self) 0)) +(define (sr-pos self) (vector-ref (jhost-state self) 1)) +(define (sr-pos! self p) (vector-set! (jhost-state self) 1 p)) +(register-host-methods! "string-reader" + (list (cons "read" (lambda (self) + (let ((s (sr-s self)) (p (sr-pos self))) + (if (>= p (string-length s)) -1.0 + (begin (sr-pos! self (+ p 1)) (->num (char->integer (string-ref s p)))))))) + (cons "mark" (lambda (self . _) (vector-set! (jhost-state self) 2 (sr-pos self)) jolt-nil)) + (cons "reset" (lambda (self) (sr-pos! self (vector-ref (jhost-state self) 2)) jolt-nil)) + (cons "skip" (lambda (self n) (let ((n (jnum->exact n))) + (sr-pos! self (min (string-length (sr-s self)) (+ (sr-pos self) n))) (->num n)))) + (cons "close" (lambda (self) jolt-nil)))) + +;; ---- PushbackReader --------------------------------------------------------- +;; state: a vector #(wrapped-reader pushed-list) +(register-class-ctor! "PushbackReader" + (lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '())))) +(define (read-unit r) ; read one code unit (flonum) from any reader, -1 at EOF + (record-method-dispatch r "read" jolt-nil)) +(register-host-methods! "pushback-reader" + (list (cons "read" (lambda (self) + (let ((pushed (vector-ref (jhost-state self) 1))) + (if (pair? pushed) + (begin (vector-set! (jhost-state self) 1 (cdr pushed)) (car pushed)) + (read-unit (vector-ref (jhost-state self) 0)))))) + (cons "unread" (lambda (self ch) + (vector-set! (jhost-state self) 1 + (cons (if (char? ch) (->num (char->integer ch)) ch) (vector-ref (jhost-state self) 1))) + jolt-nil)) + (cons "close" (lambda (self) jolt-nil)))) + +;; ---- HashMap ---------------------------------------------------------------- +;; state: a box holding an alist of (k . v), jolt= keyed. +(define (hm-alist self) (vector-ref (jhost-state self) 0)) +(define (hm-set! self al) (vector-set! (jhost-state self) 0 al)) +(define (hm-assoc al k v) + (let loop ((ps al) (acc '()) (hit #f)) + (cond ((null? ps) (reverse (if hit acc (cons (cons k v) acc)))) + ((jolt=2 (caar ps) k) (loop (cdr ps) (cons (cons k v) acc) #t)) + (else (loop (cdr ps) (cons (car ps) acc) hit))))) +(define (hm-get al k) (let loop ((ps al)) (cond ((null? ps) jolt-nil) ((jolt=2 (caar ps) k) (cdar ps)) (else (loop (cdr ps)))))) +(define (coll->pairs m) + (if (jolt-nil? m) '() + (let loop ((s (jolt-seq m)) (acc '())) + (if (jolt-nil? s) (reverse acc) + (let ((e (seq-first s))) (loop (jolt-seq (seq-more s)) (cons (cons (jolt-nth e 0) (jolt-nth e 1)) acc))))))) +(register-class-ctor! "HashMap" + (lambda args + (let ((init (and (pair? args) (car args)))) + (make-jhost "hashmap" (vector (if (and init (not (number? init))) (coll->pairs init) '())))))) +(register-host-methods! "hashmap" + (list (cons "get" (lambda (self k) (hm-get (hm-alist self) k))) + (cons "put" (lambda (self k v) (hm-set! self (hm-assoc (hm-alist self) k v)) v)) + (cons "putAll" (lambda (self m) (for-each (lambda (p) (hm-set! self (hm-assoc (hm-alist self) (car p) (cdr p)))) (coll->pairs m)) jolt-nil)) + (cons "containsKey" (lambda (self k) (not (jolt-nil? (hm-get (hm-alist self) k))))) + (cons "size" (lambda (self) (->num (length (hm-alist self))))))) + +;; ---- StringTokenizer -------------------------------------------------------- +;; state: a vector #(tokens-list pos) +(define (tokenize s delims) + (let ((dset (string->list delims))) + (let loop ((chars (string->list s)) (cur '()) (toks '())) + (cond ((null? chars) (reverse (if (null? cur) toks (cons (list->string (reverse cur)) toks)))) + ((memv (car chars) dset) + (loop (cdr chars) '() (if (null? cur) toks (cons (list->string (reverse cur)) toks)))) + (else (loop (cdr chars) (cons (car chars) cur) toks)))))) +(register-class-ctor! "StringTokenizer" + (lambda (s . delims) (make-jhost "string-tokenizer" + (vector (tokenize (if (string? s) s (jolt-str-render-one s)) + (if (null? delims) " \t\n\r\f" (car delims))) 0)))) +(register-host-methods! "string-tokenizer" + (list (cons "hasMoreTokens" (lambda (self) (< (vector-ref (jhost-state self) 1) (length (vector-ref (jhost-state self) 0))))) + (cons "countTokens" (lambda (self) (->num (- (length (vector-ref (jhost-state self) 0)) (vector-ref (jhost-state self) 1))))) + (cons "nextToken" (lambda (self) + (let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1))) + (if (< p (length toks)) + (begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p)) + (error #f "NoSuchElementException"))))))) + +;; ---- String / BigInteger / MapEntry constructors ---------------------------- +;; (String. x) / (String. bytes): a bytevector decodes UTF-8; else stringify. +(register-class-ctor! "String" + (lambda (x . _) (cond ((bytevector? x) (utf8->string x)) ((string? x) x) (else (jolt-str-render-one x))))) +(register-class-ctor! "BigInteger" + (lambda (v) (parse-int-or-throw v 10 "BigInteger"))) +(register-class-ctor! "MapEntry" (lambda (k v) (make-map-entry k v))) +;; JVM exception ctors: keep the message string (so getMessage / ex-message work). +(for-each (lambda (nm) (register-class-ctor! nm (lambda (msg . _) (if (string? msg) msg (jolt-str-render-one msg))))) + '("Exception" "RuntimeException" "IllegalArgumentException" "IllegalStateException" + "InterruptedException" "UnsupportedOperationException" "IOException")) + +;; ---- URLEncoder / URLDecoder (www-form-urlencoded) -------------------------- +(define (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))) +(define hex-digits "0123456789ABCDEF") +(define (url-encode s . _) + (let ((bs (string->utf8 (if (string? s) s (jolt-str-render-one s)))) (out '())) + (let loop ((i 0)) + (if (= i (bytevector-length bs)) (list->string (reverse out)) + (let ((b (bytevector-u8-ref bs i))) + (cond ((url-unreserved? b) (set! out (cons (integer->char b) out))) + ((= b 32) (set! out (cons #\+ out))) + (else (set! out (cons (string-ref hex-digits (bitwise-and b 15)) + (cons (string-ref hex-digits (bitwise-arithmetic-shift-right b 4)) + (cons #\% out)))))) + (loop (+ i 1))))))) +(define (hexv c) + (cond ((and (char<=? #\0 c) (char<=? c #\9)) (- (char->integer c) 48)) + ((and (char<=? #\A c) (char<=? c #\F)) (- (char->integer c) 55)) + ((and (char<=? #\a c) (char<=? c #\f)) (- (char->integer c) 87)) + (else (error #f "URLDecoder: malformed escape")))) +(define (url-decode s . _) + (let* ((str (if (string? s) s (jolt-str-render-one s))) (n (string-length str)) (out '())) + (let loop ((i 0)) + (if (>= i n) (utf8->string (u8-list->bytevector (reverse out))) + (let ((c (string-ref str i))) + (cond ((char=? c #\+) (set! out (cons 32 out)) (loop (+ i 1))) + ((char=? c #\%) + (set! out (cons (+ (* 16 (hexv (string-ref str (+ i 1)))) (hexv (string-ref str (+ i 2)))) out)) + (loop (+ i 3))) + (else (set! out (cons (char->integer c) out)) (loop (+ i 1))))))))) +(define (u8-list->bytevector lst) + (let ((bv (make-bytevector (length lst)))) + (let loop ((l lst) (i 0)) (if (null? l) bv (begin (bytevector-u8-set! bv i (car l)) (loop (cdr l) (+ i 1))))))) +(register-class-statics! "URLEncoder" (list (cons "encode" url-encode))) +(register-class-statics! "URLDecoder" (list (cons "decode" url-decode))) +(register-class-statics! "Charset" (list (cons "forName" (lambda (nm) (make-jhost "charset" (list (cons 'name nm))))))) + +;; ---- Base64 (RFC 4648) ------------------------------------------------------ +(define b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") +(define (->bytevector x) (cond ((bytevector? x) x) ((string? x) (string->utf8 x)) (else (string->utf8 (jolt-str-render-one x))))) +(define (b64-encode x) + (let* ((bs (->bytevector x)) (n (bytevector-length bs)) (out '())) + (let loop ((i 0)) + (if (>= i n) (list->string (reverse out)) + (let* ((b0 (bytevector-u8-ref bs i)) + (b1 (if (< (+ i 1) n) (bytevector-u8-ref bs (+ i 1)) #f)) + (b2 (if (< (+ i 2) n) (bytevector-u8-ref bs (+ i 2)) #f))) + (set! out (cons (string-ref b64-alphabet (bitwise-arithmetic-shift-right b0 2)) out)) + (set! out (cons (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b0 3) 4) + (bitwise-arithmetic-shift-right (or b1 0) 4))) out)) + (set! out (cons (if b1 (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b1 15) 2) + (bitwise-arithmetic-shift-right (or b2 0) 6))) #\=) out)) + (set! out (cons (if b2 (string-ref b64-alphabet (bitwise-and b2 63)) #\=) out)) + (loop (+ i 3))))))) +(define (b64-char-val c) + (let loop ((i 0)) (cond ((= i 64) (error #f "Base64: illegal character")) ((char=? (string-ref b64-alphabet i) c) i) (else (loop (+ i 1)))))) +(define (b64-decode x) + (let* ((str (let ((s (if (string? x) x (utf8->string (->bytevector x))))) + (list->string (filter (lambda (c) (not (char=? c #\=))) (string->list s))))) + (out '()) (acc 0) (bits 0)) + (for-each (lambda (c) + (set! acc (bitwise-ior (bitwise-arithmetic-shift-left acc 6) (b64-char-val c))) + (set! bits (+ bits 6)) + (when (>= bits 8) + (set! bits (- bits 8)) + (set! out (cons (bitwise-and (bitwise-arithmetic-shift-right acc bits) 255) out)))) + (string->list str)) + (u8-list->bytevector (reverse out)))) +(register-host-methods! "b64-encoder" + (list (cons "encode" (lambda (self bs) (string->utf8 (b64-encode bs)))) + (cons "encodeToString" (lambda (self bs) (b64-encode bs))))) +(register-host-methods! "b64-decoder" + (list (cons "decode" (lambda (self s) (b64-decode s))))) +(register-class-statics! "Base64" + (list (cons "getEncoder" (lambda () (make-jhost "b64-encoder" '()))) + (cons "getDecoder" (lambda () (make-jhost "b64-decoder" '()))))) + +;; ---- java.util.regex.Pattern ------------------------------------------------ +;; Pattern/compile returns a jolt-regex value (regex-t), so str/replace, re-find, +;; .split etc. accept it transparently. +(define pattern-multiline 8.0) +(define (pattern-quote s) + (let ((meta "\\.[]{}()*+-?^$|&") (s (if (string? s) s (jolt-str-render-one s))) (out '())) + (let loop ((i 0)) + (if (= i (string-length s)) (list->string (reverse out)) + (let ((c (string-ref s i))) + (when (memv c (string->list meta)) (set! out (cons #\\ out))) + (set! out (cons c out)) + (loop (+ i 1))))))) +(register-class-statics! "Pattern" + (list (cons "compile" (lambda (s . flags) + (if (and (pair? flags) (= (bitwise-and (jnum->exact (car flags)) 8) 8)) + (jolt-regex (string-append "(?m)" s)) + (jolt-regex s)))) + (cons "quote" (lambda (s) (pattern-quote s))) + (cons "MULTILINE" pattern-multiline))) +;; record-method-dispatch already routes string? -> jolt-string-method. Add a +;; regex-t arm (Pattern .split / .matcher-less surface used by corpus) by wrapping +;; once more — a regex-t isn't a jhost. +(define %hs-rmd2 record-method-dispatch) +(set! record-method-dispatch + (lambda (obj method-name rest-args) + (if (regex-t? obj) + (let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))) + (cond ((string=? method-name "split") + ;; .split returns a String[] — a seq, like the seed's array (prints + ;; (a b c), not a vector). re-split with no limit; drop trailing + ;; empties (JVM default). + (let ((parts (re-split (regex-t-irx obj) (car rest) #f))) + (list->cseq (str-split-drop-trailing parts)))) + ((string=? method-name "pattern") (regex-t-source obj)) + (else (error #f (string-append "No method " method-name " on Pattern"))))) + (%hs-rmd2 obj method-name rest-args)))) + +;; ---- def-var! the registry entry points so emit can also reach them --------- +(def-var! "clojure.core" "host-static-ref" host-static-ref) +(def-var! "clojure.core" "host-static-call" (lambda (c m . a) (apply host-static-call c m a))) +(def-var! "clojure.core" "host-new" (lambda (c . a) (apply host-new c a))) diff --git a/host/chez/jolt-chez.janet b/host/chez/jolt-chez.janet index db0e79d..9dda80b 100644 --- a/host/chez/jolt-chez.janet +++ b/host/chez/jolt-chez.janet @@ -22,6 +22,7 @@ "host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss" "host/chez/ns.ss" "host/chez/post-prelude.ss" "host/chez/natives-meta.ss" "host/chez/natives-str.ss" "host/chez/records.ss" + "host/chez/host-static.ss" "src/jolt/clojure/string.clj"] (array/push parts (slurp f))) (string/slice (string (hash (string/join parts))) 0)) diff --git a/host/chez/rt.ss b/host/chez/rt.ss index e2b4794..d0c32f1 100644 --- a/host/chez/rt.ss +++ b/host/chez/rt.ss @@ -271,3 +271,9 @@ ;; a string target. After regex.ss (jolt-re-pattern/regex-t-irx) + records.ss ;; (which references jolt-string-method). (load "host/chez/natives-str.ss") + +;; host class statics + constructors (jolt-avt6, Phase 2): host-static-ref/ +;; host-static-call/host-new + the jhost method registry. Loads LAST — it extends +;; record-method-dispatch (records.ss) and reuses natives-str helpers (str-trim, +;; ascii-string-down, re-split, str-split-drop-trailing) + the regex-t accessors. +(load "host/chez/host-static.ss") diff --git a/jolt-core/jolt/analyzer.clj b/jolt-core/jolt/analyzer.clj index 1e85130..7cfe8d0 100644 --- a/jolt-core/jolt/analyzer.clj +++ b/jolt-core/jolt/analyzer.clj @@ -16,7 +16,7 @@ them to one keeps the compiled namespace simple." (:require [jolt.ir :refer [const local var-ref the-var host-ref if-node do-node invoke def-node let-node fn-node vector-node map-node set-node - quote-node throw-node]] + quote-node throw-node host-static host-new]] [jolt.host :refer [form-sym? form-sym-name form-sym-ns form-list? form-vec? form-map? form-set? form-char? form-literal? form-elements form-vec-items @@ -294,6 +294,20 @@ :target (analyze ctx (nth items 1) env) :args (mapv #(analyze ctx % env) (drop 2 items))}) +;; A constructor head: `Class.` — a symbol ending in "." (but not the member +;; access `.method` / `..` forms). `(Class. args*)` builds an instance. +(defn- ctor-head? [nm] + (and (> (count nm) 1) + (= "." (subs nm (dec (count nm)) (count nm))) + (not (= "." (subs nm 0 1))))) + +;; `(Class. args*)` and `(new Class args*)` -> a :host-new node carrying the class +;; token and the analyzed args. The Janet back end punts it (the interpreter runs +;; the constructor from its class-ctors registry); the Chez back end lowers it to +;; a runtime constructor dispatch (jolt-avt6). +(defn- analyze-ctor [ctx class args env] + (host-new class (mapv #(analyze ctx % env) args))) + (defn- analyze-symbol [ctx form env] (let [nm (form-sym-name form) ns (form-sym-ns form)] (cond @@ -302,7 +316,13 @@ ns (let [r (resolve-global ctx form)] (if (= :var (:kind r)) (var-ref (:ns r) (:name r)) - (uncompilable (str "qualified ref " ns "/" nm)))) + ;; A non-var qualified ref `Class/member` is a host class static + ;; (Math/sqrt, Long/MAX_VALUE, System/getenv). The Janet back end + ;; punts the :host-static node (the interpreter resolves it from its + ;; class-statics registry, exactly as it did when this was an + ;; uncompilable); the Chez back end lowers it to a runtime static + ;; dispatch (jolt-avt6). + (host-static ns nm))) :else (let [r (resolve-global ctx form)] (case (:kind r) :var (var-ref (:ns r) (:name r)) @@ -337,6 +357,13 @@ (analyze-special ctx hname items env) (and hname (not shadowed) (method-head? hname)) (analyze-host-call ctx hname items env) + ;; (Class. args*) — trailing-dot constructor sugar. + (and hname (not shadowed) (ctor-head? hname)) + (analyze-ctor ctx (subs hname 0 (dec (count hname))) (rest items) env) + ;; (new Class args*) — explicit constructor. + (and (= hname "new") (not shadowed) (>= (count items) 2) + (form-sym? (nth items 1))) + (analyze-ctor ctx (form-sym-name (nth items 1)) (drop 2 items) env) (and hname (not shadowed) (form-special? hname)) (uncompilable (str "special form " hname)) (and (form-sym? head) (not shadowed) (form-macro? ctx head)) diff --git a/jolt-core/jolt/ir.clj b/jolt-core/jolt/ir.clj index dfa8f46..c68f4ab 100644 --- a/jolt-core/jolt/ir.clj +++ b/jolt-core/jolt/ir.clj @@ -27,6 +27,18 @@ ;; Janet) — the back end emits a host-appropriate reference. (defn host-ref [name] {:op :host :name name}) +;; A qualified static reference to a host class member, `Class/member` (e.g. +;; Math/sqrt, Long/MAX_VALUE, System/getenv). A leaf node carrying the class and +;; member names. The Janet back end punts it (the interpreter resolves the static +;; from its class-statics registry); the Chez back end lowers a value ref to +;; host-static-ref and a call head to host-static-call (host-static.ss). +(defn host-static [class member] {:op :host-static :class class :member member}) + +;; A host constructor, `(Class. args*)` / `(new Class args*)`. Carries the class +;; name and the analyzed argument nodes. Janet punts (interpreter runs the +;; constructor); Chez lowers to host-new (host-static.ss class-ctor registry). +(defn host-new [class args] {:op :host-new :class class :args args}) + (defn if-node [test then else] {:op :if :test test :then then :else else}) (defn do-node [statements ret] {:op :do :statements statements :ret ret}) @@ -103,6 +115,7 @@ (= op :def) (assoc node :init (f (get node :init))) (= op :host-call) (assoc node :target (f (get node :target)) :args (mapv f (get node :args))) + (= op :host-new) (assoc node :args (mapv f (get node :args))) ;; :catch-body / :finally are optional; recurse them only when PRESENT. ;; Assoc'ing them nil-when-absent would turn the node into a phm (jolt's ;; nil-valued-key representation) and force backend densification — so we @@ -112,5 +125,5 @@ n (if (get node :catch-body) (assoc n :catch-body (f (get node :catch-body))) n) n (if (get node :finally) (assoc n :finally (f (get node :finally))) n)] n) - ;; :const :local :var :host :the-var :rt :quote — no child nodes + ;; :const :local :var :host :host-static :the-var :rt :quote — no child nodes :else node))) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index c3ef2a7..63888d3 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -781,6 +781,13 @@ # punt to the interpreter, exactly as the analyzer used to before producing # a :host-call node (the Chez back end lowers it instead). :host-call (error "jolt/uncompilable: host method call") + # host class statics (Class/member) and constructors ((Class. ...)/(new + # Class ...)): the back end doesn't model class interop — punt to the + # interpreter (its class-statics / class-ctors registries), exactly as the + # analyzer used to before producing these nodes. The Chez back end lowers + # them to a runtime static/ctor dispatch (jolt-avt6). + :host-static (error "jolt/uncompilable: host static ref") + :host-new (error "jolt/uncompilable: host constructor") # regex literal: the back end doesn't compile patterns — punt to the # interpreter (the seed compiles #"…" to a Janet PEG). Chez emits jolt-regex. :regex (error "jolt/uncompilable: regex literal") diff --git a/test/chez/_javastatic.janet b/test/chez/_javastatic.janet new file mode 100644 index 0000000..d8c4652 --- /dev/null +++ b/test/chez/_javastatic.janet @@ -0,0 +1,91 @@ +# jolt-avt6 — host class statics + constructors on Chez. The analyzer lowers +# Class/member to :host-static and (Class. ...) to :host-new; the Chez emit lowers +# them to host-static-ref/host-static-call/host-new (host-static.ss registry). +# Expectations are the build/jolt (seed) oracle, captured per case. +# +# janet test/chez/_javastatic.janet +(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/jolt-chez")) + +# [label expr] — expected is whatever build/jolt prints (captured at runtime). +(def exprs + ["(Math/sqrt 16)" + "(Math/abs -3)" + "(Math/max 2 7)" + "(pos? Long/MAX_VALUE)" + "(String/valueOf 42)" + "(String/valueOf \"hi\")" + "(String/valueOf :k)" + "(String/valueOf nil)" + "(Long/parseLong \"42\")" + "(Long/valueOf \"42\")" + "(Integer/parseInt \"ff\" 16)" + "(.byteValue (Integer/valueOf \"ff\" 16))" + "(Boolean/parseBoolean \"true\")" + "(Boolean/parseBoolean \"yes\")" + "(Character/isUpperCase \\A)" + "(Character/isLowerCase \\a)" + "(Character/isUpperCase \\a)" + "(Thread/interrupted)" + "(System/getProperty \"os.name\")" + "(string? (get (System/getenv) \"HOME\"))" + "(string? (System/getenv \"HOME\"))" + "(fn? System/exit)" + "(string? (get (System/getProperties) \"os.name\"))" + "(pos? (count (seq (System/getenv))))" + "(let [es (map (fn [[k v]] [k v]) (System/getenv))] (and (pos? (count es)) (every? vector? es)))" + # constructors + their methods + "(.toString (StringBuilder. \"x\"))" + "(.toString (-> (StringBuilder.) (.append \"a\") (.append \\b) (.append 1)))" + "(.toString (.append (StringBuilder. 16) \"x\"))" + "(let [sb (StringBuilder.)] (.append sb \"abcd\") (.setLength sb 2) (.toString sb))" + "(let [w (StringWriter.)] (.write w \"a\") (.append w \\b) (.toString w))" + "(let [r (StringReader. \"ab\")] [(.read r) (.read r) (.read r)])" + "(let [r (StringReader. \"ab\")] (.mark r 1) [(.read r) (do (.reset r) (.read r))])" + "(let [r (java.io.PushbackReader. (java.io.StringReader. \"ab\"))] [(.read r) (.read r)])" + "(let [r (PushbackReader. (StringReader. \"ab\")) a (.read r)] (.unread r a) [a (.read r) (.read r)])" + "(let [r (PushbackReader. (StringReader. \"a\"))] (.unread r \\x) [(.read r) (.read r)])" + "(BigInteger. \"123\")" + "(let [m (HashMap. {:a 1 :b 2})] (.get m :b))" + "(let [m (HashMap. {})] (.put m :x 1) (.put m :y 2) (.size m))" + "(let [t (StringTokenizer. \"a=1&b=2\" \"&\")] [(.nextToken t) (.nextToken t)])" + "(.toString (StringBuilder. \"x\"))" + # ring-codec surface + "(URLEncoder/encode \"a b=c\")" + "(URLDecoder/decode (URLEncoder/encode \"x &=%?\"))" + "(String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))" + "(String. (.decode (Base64/getDecoder) (String. (.encode (Base64/getEncoder) (.getBytes \"hello\")))))" + "(Integer/parseInt \"ff\" 16)" + # Pattern statics + "(regex? (Pattern/compile \"a.c\"))" + "(.split (Pattern/compile \",\") \"a,b,c\")" + "(do (require '[clojure.string :as s]) (s/replace \"a1b2\" (Pattern/compile \"[0-9]\") \"\"))" + "(boolean (re-find (Pattern/compile \"^x\" Pattern/MULTILINE) \"y\\nx\"))" + "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"za.cy\"))" + "(boolean (re-find (re-pattern (Pattern/quote \"a.c\")) \"zabcy\"))"]) + +(defn run-capture [bin expr] + (def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe})) + (def out (ev/read (proc :out) 0x100000)) + (def err (ev/read (proc :err) 0x100000)) + (def code (os/proc-wait proc)) + (def lines (filter (fn [l] (not (empty? l))) + (string/split "\n" (string/trim (if out (string out) ""))))) + [code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))]) + +(var pass 0) +(def fails @[]) +(each expr exprs + (def [ocode oracle _] (run-capture "build/jolt" expr)) + (def [code got err] (run-capture jolt-bin expr)) + (cond + (not= ocode 0) (array/push fails [expr (string "ORACLE FAILED exit " ocode)]) + (not= code 0) (array/push fails [expr (string "exit " code "; err: " err)]) + (= got oracle) (++ pass) + (array/push fails [expr (string "want `" oracle "`, got `" got "`")]))) + +(printf "\n_javastatic parity [%s]: %d/%d passed" jolt-bin pass (length exprs)) +(when (> (length fails) 0) + (printf "%d FAIL(s):" (length fails)) + (each [e m] fails (printf " FAIL %s\n %s" e m))) +(flush) +(os/exit (if (empty? fails) 0 1)) diff --git a/test/chez/emit-test.janet b/test/chez/emit-test.janet index 6106a59..2f526ee 100644 --- a/test/chez/emit-test.janet +++ b/test/chez/emit-test.janet @@ -300,14 +300,17 @@ (string "chez=" out " janet=" want " | " err)))) # 3l) host interop method calls (inc 3h). (.method target arg*) analyzes to a -# :host-call IR node and lowers to a jolt-host-call dispatch. The Janet back end -# PUNTS these (no interop model -> interpreter); the Chez RT shims the methods -# jolt-core's io tier uses: .write -> display to a port, .isDirectory -> -# file-directory?, .listFiles -> directory-list. Interop has no portable oracle -# (the Janet host models it differently), so these are emit-shape checks plus one -# deterministic runtime probe (the root "/" is always a directory). +# :host-call IR node and lowers to a dispatch. The Janet back end PUNTS these +# (no interop model -> interpreter); the Chez RT shims the File methods +# jolt-core's io tier uses via jolt-host-call (.isDirectory -> file-directory?, +# .listFiles -> directory-list). All OTHER methods (.write/.append/.read/... on +# a StringWriter/StringReader/record) route through record-method-dispatch +# (jolt-avt6 removed .write from the jolt-host-call fast-path so a StringWriter +# jhost handles it). Interop has no portable oracle (the Janet host models it +# differently), so these are emit-shape checks plus one deterministic runtime +# probe (the root "/" is always a directory). (each [label src needle] - [["emit .write -> jolt-host-call" "(fn [w x] (.write w x))" "jolt-host-call"] + [["emit .write -> record-method-dispatch" "(fn [w x] (.write w x))" "record-method-dispatch"] ["emit .write keeps method name" "(fn [w x] (.write w x))" "\"write\""] ["emit .isDirectory -> jolt-host-call" "(fn [f] (.isDirectory f))" "isDirectory"] ["emit .listFiles -> jolt-host-call" "(fn [f] (.listFiles f))" "listFiles"]] @@ -318,6 +321,21 @@ (ok "runtime .isDirectory \"/\" = true" (and (= code 0) (= out "true")) (string "chez=" out " | " err))) +# 3l') host class statics + constructors (jolt-avt6). Class/member lowers to a +# :host-static node (host-static-ref in value position, host-static-call as a +# call head); (Class. ...) / (new Class ...) lower to :host-new. The Janet back +# end punts all three; the Chez RT resolves them from the class-statics / +# class-ctors / jhost-method registries (host/chez/host-static.ss). Emit-shape +# checks; the registry behaviour is covered by test/chez/_javastatic.janet. +(each [label src needle] + [["emit Class/field -> host-static-ref" "(fn [] Long/MAX_VALUE)" "(host-static-ref \"Long\" \"MAX_VALUE\")"] + ["emit Class/method call -> host-static-call" "(fn [] (System/getenv))" "(host-static-call \"System\" \"getenv\")"] + ["emit static call passes args" "(fn [x] (Long/parseLong x))" "(host-static-call \"Long\" \"parseLong\""] + ["emit (Class.) -> host-new" "(fn [] (StringBuilder.))" "(host-new \"StringBuilder\")"] + ["emit (new Class ...) -> host-new" "(fn [s] (new java.io.StringReader s))" "(host-new \"java.io.StringReader\""]] + (let [scm (protect (emit/emit (backend/analyze-form ctx (in (r/parse-next src) 0))))] + (ok label (and (scm 0) (string/find needle (scm 1))) (string/format "%p" scm)))) + # 3m) regex (jolt-i0s3): the #"…" literal lowers to a jolt-regex value over the # vendored irregex; re-pattern/re-matches/re-find/re-seq/regex? are def-var!'d # into clojure.core (not subset native-ops — irregex's Unicode/property diff --git a/test/chez/run-corpus-prelude.janet b/test/chez/run-corpus-prelude.janet index 049eca2..62d8755 100644 --- a/test/chez/run-corpus-prelude.janet +++ b/test/chez/run-corpus-prelude.janet @@ -45,9 +45,10 @@ {"class name evaluates to canonical string" true "dispatch-only class name" true "inside class" true - "values evaluate in source order" true - "keys evaluate before their values, pairwise" true - "source order with a nil value (phm form)" true + # (the collection-literal eval-order entries here — "values evaluate in source + # order" / "keys evaluate before their values" / "source order with a nil value" + # — were REMOVED in jolt-avt6: emit now evaluates vector/set/map literal + # elements left-to-right via emit-ordered, so they pass.) "close on throw" true # *ns* now a namespace value (jolt-yxqm): str/ns-name of *ns* + the var str # case ("ns-name of *ns*" / "str of *ns*" / "*ns* user" / "str of a var") pass. @@ -69,19 +70,21 @@ "defmethod overrides a record, top level" true "defmethod fires nested in a map" true "defmethod fires through prn" true + # Same print-method gap, newly REACHABLE in jolt-avt6: StringWriter now + # constructs, so (defmethod print-method :number ...) + (print-method 42 w) + # runs — but print-method's multimethod override on a builtin isn't consulted, + # so the StringWriter holds "42" not "#42#". (The :default print-method path — + # "StringWriter accumulates" / "direct call uses :default" — does pass now.) + "direct builtin override" true # var def-time metadata (^:private / ^Type tag / docstring) is now captured on # the Chez var-cell (jolt-zikh), so those three cases pass. "methods table inspectable" true # jolt-nfca made (require ...) a runtime no-op (the driver pre-evals requires # for aliases), and the clojure.string prelude tier now loads — which makes # these previously-CRASHING cases emit + run, surfacing pre-existing gaps: - # - the read-line trio reads two lines into a [(read-line) (read-line)] vector; - # the emitted Scheme evaluates the two elements in non-source order (the same - # eval-order gap already allowlisted above as "values evaluate in source - # order"), so the lines come back swapped. Reachable now that with-in-str runs. - "read-line sequential" true - "read-line after last" true - "empty line" true + # - the read-line trio ([(read-line) (read-line)]) now PASSES: jolt-avt6's + # emit-ordered evaluates the two vector elements left-to-right, so the lines + # no longer come back swapped (the entries were removed here). # - (instance? clojure.lang.Atom (atom 0)): the fully-qualified host class name # clojure.lang.Atom isn't mapped to the atom predicate on Chez (host-class # interop, jolt-mn9o/avt6). Reachable now that the leading require is a no-op. @@ -220,8 +223,18 @@ # join/split/replace/replace-all/reverse-b) def-var!'d on the RT; regex split # keeps interior empties + honors limit, regex replace does $N + fn replacement; # require/use are runtime no-ops) 2078. +# jolt-avt6 (host class statics + constructors — the analyzer lowers Class/member +# to :host-static and (Class. ...)/(new Class ...) to :host-new; the Chez RT +# resolves them from class-statics/class-ctors/jhost-method registries +# (host-static.ss): Math/System/Long/Integer/Boolean/Character/String/Thread/Class +# statics, Pattern compile/quote/MULTILINE, URLEncoder/Decoder, Base64, the Number +# method surface (byteValue/intValue/...), and the StringBuilder/StringWriter/ +# StringReader/PushbackReader/HashMap/StringTokenizer/BigInteger/String/MapEntry/ +# exception constructors. Also emit now evaluates collection-literal elements +# left-to-right (emit-ordered), which un-allowlisted the 6 eval-order cases. +# java.time formatting / edn-read-over-readers / slurp-over-readers deferred) 2134. # Strided runs scale down. -(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2078"))) +(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2134"))) (def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor)) (when (or (> (length diverged) 0) (< pass floor)) (printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))