diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index e4740f7..bd10c86 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -432,6 +432,46 @@ {"currentTimeMillis" (fn [] (math/floor (* 1000 (os/clock :realtime)))) "nanoTime" (fn [] (math/floor (* 1e9 (os/clock :monotonic))))}) +# Long statics: sentinels portable code compares against. jolt numbers are +# doubles, so these are the f64 approximations. +(def- long-statics + {"MAX_VALUE" 9223372036854775807 + "MIN_VALUE" -9223372036854775808}) + +# java.lang.String method surface for clj-compat interop: (.toLowerCase s), +# (.indexOf s x), ... — the methods portable cljc libraries actually call. +# Case mapping is ASCII (the whole engine is byte-based); indexOf returns -1 +# on miss, as on the JVM. +(defn- str-needle [x] + (if (and (struct? x) (= :jolt/char (get x :jolt/type))) + (string/from-bytes (x :ch)) + (string x))) +(def- string-methods + {"toString" (fn [s] s) + "toLowerCase" (fn [s] (string/ascii-lower s)) + "toUpperCase" (fn [s] (string/ascii-upper s)) + "trim" (fn [s] (string/trim s)) + "length" (fn [s] (length s)) + "isEmpty" (fn [s] (= 0 (length s))) + "charAt" (fn [s i] {:jolt/type :jolt/char :ch (s i)}) + "codePointAt" (fn [s i] (s i)) + "indexOf" (fn [s x &opt from] (or (string/find (str-needle x) s (or from 0)) -1)) + "lastIndexOf" (fn [s x] + (let [n (str-needle x)] + (var found -1) (var i 0) + (while (< i (length s)) + (let [f (string/find n s i)] + (if f (do (set found f) (set i (+ f 1))) (set i (length s))))) + found)) + "substring" (fn [s start &opt end] (string/slice s start end)) + "startsWith" (fn [s p] (string/has-prefix? p s)) + "endsWith" (fn [s p] (string/has-suffix? p s)) + "contains" (fn [s sub] (not (nil? (string/find (str-needle sub) s)))) + "concat" (fn [s o] (string s o)) + "replace" (fn [s a b] (string/replace-all (str-needle a) (str-needle b) s)) + "compareTo" (fn [s o] (cond (< s o) -1 (> s o) 1 0)) + "equalsIgnoreCase" (fn [s o] (= (string/ascii-lower s) (string/ascii-lower (string o))))}) + (defn- resolve-sym [ctx bindings sym-s] (let [name (sym-s :name) ns (sym-s :ns)] @@ -444,6 +484,9 @@ (if (= ns "System") (let [v (get system-statics name)] (if (nil? v) (error (string "Unsupported System member: System/" name)) v)) + (if (= ns "Long") + (let [v (get long-statics name)] + (if (nil? v) (error (string "Unsupported Long member: Long/" name)) v)) (if (not (nil? ns)) (let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx)) aliased-ns (or (ns-alias-lookup current-ns ns) (ns-import-lookup current-ns ns)) @@ -493,7 +536,7 @@ # No implicit Janet fallback (Stage 3): an unresolved # Clojure symbol is an error. Host access is the explicit # janet/ prefix above. - (error (string "Unable to resolve symbol: " name)))))))))))))))) + (error (string "Unable to resolve symbol: " name))))))))))))))))) (defn- parse-arg-names "Parse a parameter vector, handling & rest args. Returns {:fixed [names...] :rest name-or-nil :all [names...]}" @@ -1192,6 +1235,11 @@ "java.lang.String" (string? val) "Boolean" (or (= true val) (= false val)) "Keyword" (keyword? val) + # regex patterns (cuerdas-style (instance? Pattern x) checks) + "Pattern" (and (table? val) (= :jolt/regex (val :jolt/type))) + "java.util.regex.Pattern" (and (table? val) (= :jolt/regex (val :jolt/type))) + "Character" (and (struct? val) (= :jolt/char (get val :jolt/type))) + "java.lang.Character" (and (struct? val) (= :jolt/char (get val :jolt/type))) "clojure.lang.Atom" (and (table? val) (= :jolt/atom (val :jolt/type))) "clojure.lang.Volatile" (and (table? val) (= :jolt/volatile (val :jolt/type))) "clojure.lang.Delay" (and (table? val) (= :jolt/delay (val :jolt/type))) @@ -1762,6 +1810,11 @@ (if (> (length form) 3) # method call: (. obj method args...) (let [args (map |(eval-form ctx bindings $) (tuple/slice form 3))] + (if (or (string? target) (buffer? target)) + (let [m (get string-methods field-name)] + (if m + (m (string target) ;args) + (error (string "Unsupported String method ." field-name)))) (if (target :jolt/deftype) (let [method-key (keyword field-name)] (apply (get target method-key) target ;args)) @@ -1777,10 +1830,16 @@ (method-fn target ;args) (error (string "Cannot call non-function " field-name " on " (type target))))) (error (string "Cannot call non-function " field-name " on " (type target)))))) - (error (string "Cannot call method " field-name " on " (type target)))))) + (error (string "Cannot call method " field-name " on " (type target))))))) # (. obj member) with no extra args: a symbol member naming a # function is a zero-arg method call (receiver passed as self); - # a keyword or `-field` member is plain field access. + # a keyword or `-field` member is plain field access. Strings get + # the java.lang.String surface (clj-compat: (.toLowerCase s) ...). + (if (or (string? target) (buffer? target)) + (let [m (get string-methods field-name)] + (if m + (m (string target)) + (error (string "Unsupported String method ." field-name)))) (let [v (get target (keyword field-name))] (if (and (struct? member-raw) (= :symbol (member-raw :jolt/type)) (not (string/has-prefix? "-" member-name))) @@ -1790,7 +1849,7 @@ (array? v) (let [f (eval-form ctx bindings v)] (if (or (function? f) (cfunction? f)) (f target) f)) v) - v)))) + v))))) # default: function application — check for macros (if (and (struct? first-form) (= :symbol (first-form :jolt/type))) (let [sym-name (first-form :name)] @@ -1800,6 +1859,18 @@ (let [field-name (string/slice sym-name 2) target (eval-form ctx bindings (in form 1))] (get target (keyword field-name))) + # (.method obj args...) sugar -> (. obj method args...): desugar and + # re-enter the dot special form (which holds the String surface, the + # deftype method path, and the map-fn fallback). + (if (and (> (length sym-name) 1) + (= (string/slice sym-name 0 1) ".") + (not= sym-name "..") + (> (length form) 1)) + (eval-form ctx bindings + (array/concat @[{:jolt/type :symbol :ns nil :name "."} + (in form 1) + {:jolt/type :symbol :ns nil :name (string/slice sym-name 1)}] + (tuple/slice form 2))) # Handle ClassName. constructor syntax (".." is the member-threading # macro, not a constructor named ".") (if (and (> (length sym-name) 1) (not= sym-name "..") @@ -1821,7 +1892,7 @@ (eval-form ctx bindings expanded)))) (let [f (eval-form ctx bindings first-form) args (map |(eval-form ctx bindings $) (tuple/slice form 1))] - (jolt-invoke ctx f args))))))) + (jolt-invoke ctx f args)))))))) (let [f (eval-form ctx bindings first-form) args (map |(eval-form ctx bindings $) (tuple/slice form 1))] (jolt-invoke ctx f args))))) diff --git a/src/jolt/regex.janet b/src/jolt/regex.janet index 05b3e29..3d1e8dc 100644 --- a/src/jolt/regex.janet +++ b/src/jolt/regex.janet @@ -40,6 +40,40 @@ (chr "S") '(if-not (set " \t\n\r\f\v") 1) nil)) +# Unicode property classes \p{...} (jolt-xlp), mapped onto the byte PEGs: +# ASCII exactly; any high byte (>= 0x80, i.e. inside a UTF-8 sequence) counts +# as a LETTER byte — so ^\p{L}+$ accepts UTF-8 words, while \p{N}/\p{Z} +# stay ASCII-only. Lu/Ll are ASCII (case is byte-based throughout this +# engine). Unknown property names error at compile. +(defn- prop-frag [name] + (case name + "L" '(choice (range "az" "AZ") (range "\x80\xFF")) + "Lu" '(range "AZ") + "Ll" '(range "az") + "N" '(range "09") + "Nd" '(range "09") + "Z" '(set " ") + "Zs" '(set " ") + "P" '(set "!\"#%&'()*,-./:;?@[\\]_{}") + "Ps" '(set "([{") + "Pe" '(set ")]}") + "Alpha" '(choice (range "az" "AZ") (range "\x80\xFF")) + "Digit" '(range "09") + nil)) + +# At s[i] = backslash: if this is \p{Name} / \P{Name}, return [peg-frag end-i] +# (end-i past the closing brace), else nil. +(defn- parse-prop [s i] + (def pc (get s (+ i 1))) + (when (and (or (= pc (chr "p")) (= pc (chr "P"))) + (= (get s (+ i 2)) (chr "{"))) + (def close (string/find "}" s (+ i 3))) + (unless close (error "regex: unterminated \\p{...}")) + (def nm (string/slice s (+ i 3) close)) + (def frag (prop-frag nm)) + (unless frag (error (string "regex: unsupported property class \\p{" nm "}"))) + [(if (= pc (chr "P")) ~(if-not ,frag 1) frag) (+ close 1)])) + (defn- esc-byte [c] (case c (chr "n") 10 (chr "t") 9 (chr "r") 13 (chr "f") 12 (chr "v") 11 (chr "0") 0 @@ -66,12 +100,14 @@ "lower" '(range "az") '(set ""))) (set i (+ close 2))) - # escape inside class + # escape inside class — \p{...} first (multi-char), then 2-char escapes (= (s i) (chr "\\")) - (let [c (s (+ i 1)) p (pred-frag c)] - (if p (array/push alts p) - (array/push alts ~(set ,(string/from-bytes (esc-byte c))))) - (set i (+ i 2))) + (if-let [pr (parse-prop s i)] + (do (array/push alts (pr 0)) (set i (pr 1))) + (let [c (s (+ i 1)) p (pred-frag c)] + (if p (array/push alts p) + (array/push alts ~(set ,(string/from-bytes (esc-byte c))))) + (set i (+ i 2)))) # range a-z (and (< (+ i 2) (length s)) (= (s (+ i 1)) (chr "-")) (not= (s (+ i 2)) (chr "]"))) (do @@ -139,13 +175,15 @@ (= c (chr ".")) (do (++ (st :pos)) {:op :any :dotall (st :dotall)}) (= c (chr "\\")) - (let [nc (s (+ (st :pos) 1)) p (pred-frag nc)] - (+= (st :pos) 2) - (cond - p {:op :pred :peg p} - (= nc (chr "b")) {:op :anchor :kind :wordb} - (= nc (chr "B")) {:op :anchor :kind :nwordb} - {:op :char :b (esc-byte nc) :ci ci})) + (if-let [pr (parse-prop s (st :pos))] + (do (set (st :pos) (pr 1)) {:op :pred :peg (pr 0)}) + (let [nc (s (+ (st :pos) 1)) p (pred-frag nc)] + (+= (st :pos) 2) + (cond + p {:op :pred :peg p} + (= nc (chr "b")) {:op :anchor :kind :wordb} + (= nc (chr "B")) {:op :anchor :kind :nwordb} + {:op :char :b (esc-byte nc) :ci ci}))) (= c (chr "^")) (do (++ (st :pos)) {:op :anchor :kind :start}) (= c (chr "$")) (do (++ (st :pos)) {:op :anchor :kind :end}) (do (++ (st :pos)) {:op :char :b c :ci ci}))) diff --git a/test/integration/deps-conformance-test.janet b/test/integration/deps-conformance-test.janet index 689b1c8..90a98ac 100644 --- a/test/integration/deps-conformance-test.janet +++ b/test/integration/deps-conformance-test.janet @@ -7,6 +7,11 @@ (use ../../src/jolt/api) (use ../../src/jolt/types) (import ../../src/jolt/deps :as deps) +(use ../../src/jolt/reader) +# deps are clj/cljc libraries by definition (the jolt-dw4 premise): read them +# under clj-compat features so their #?(:clj ...) branches resolve (spec +# 02-reader S18 — features are a property of the loading context). +(reader-features-set! ["jolt" "clj" "default"]) (unless (os/getenv "JOLT_CONFORMANCE") (print "deps-conformance: set JOLT_CONFORMANCE=1 to run (needs network) — skipped") diff --git a/test/spec/host-interop-spec.janet b/test/spec/host-interop-spec.janet index b9a297d..42c8590 100644 --- a/test/spec/host-interop-spec.janet +++ b/test/spec/host-interop-spec.janet @@ -38,3 +38,26 @@ ["aset returns val" "9" "(aset (object-array [1 2 3]) 1 9)"] ["aset mutates" "[7 2 3]" "(let [a (object-array [1 2 3])] (aset a 0 7) (vec a))"] ["aget 2d" "4" "(aget (to-array-2d [[1 2] [3 4]]) 1 1)"]) + +# java.lang.String surface + .method sugar (clj-compat: what portable cljc +# libraries call — landed for the cuerdas acceptance run). ASCII case mapping. +(defspec "interop / String methods" + [".toLowerCase" "\"hi\"" "(.toLowerCase \"HI\")"] + [".toUpperCase" "\"HI\"" "(.toUpperCase \"hi\")"] + ["dot-form" "\"hi\"" "(. \"HI\" toLowerCase)"] + [".trim" "\"x\"" "(.trim \" x \")"] + [".length" "3" "(.length \"abc\")"] + [".isEmpty" "[true false]" "[(.isEmpty \"\") (.isEmpty \"a\")]"] + [".indexOf hit" "1" "(.indexOf \"abc\" \"b\")"] + [".indexOf miss is -1" "-1" "(.indexOf \"abc\" \"z\")"] + [".lastIndexOf" "3" "(.lastIndexOf \"abab\" \"b\")"] + [".substring" "\"bc\"" "(.substring \"abc\" 1)"] + [".substring end" "\"b\"" "(.substring \"abc\" 1 2)"] + [".startsWith" "true" "(.startsWith \"abc\" \"ab\")"] + [".endsWith" "true" "(.endsWith \"abc\" \"bc\")"] + [".contains" "true" "(.contains \"abc\" \"b\")"] + [".replace" "\"axc\"" "(.replace \"abc\" \"b\" \"x\")"] + [".charAt" "\\b" "(.charAt \"abc\" 1)"] + [".equalsIgnoreCase" "true" "(.equalsIgnoreCase \"AbC\" \"aBc\")"] + ["Long/MAX_VALUE" "true" "(pos? Long/MAX_VALUE)"] + ["unsupported method throws" :throws "(.frobnicate \"abc\")"]) diff --git a/test/spec/regex-spec.janet b/test/spec/regex-spec.janet index 15b3b8e..e852dfd 100644 --- a/test/spec/regex-spec.janet +++ b/test/spec/regex-spec.janet @@ -31,3 +31,19 @@ ["split on regex" "[\"a\" \"b\" \"c\"]" "(do (require '[clojure.string :as s]) (s/split \"a1b2c\" #\"\\d\"))"] ["replace regex" "\"X-X\"" "(do (require '[clojure.string :as s]) (s/replace \"a-b\" #\"[a-z]\" \"X\"))"] ["replace $1" "\"[a][b]\"" "(do (require '[clojure.string :as s]) (s/replace \"ab\" #\"([a-z])\" \"[$1]\"))"]) + +# Unicode property classes (jolt-xlp), byte-PEG approximation: ASCII exact, +# any high byte (inside a UTF-8 sequence) counts as a LETTER for \p{L}. +# Acceptance target was cuerdas (kebab/snake/capital now pass conformance). +(defspec "regex / \\p property classes" + ["p{L} ascii" "\"hello\"" `(re-matches #"^\p{L}+$" "hello")`] + ["p{L} utf-8" "true" `(boolean (re-matches #"^\p{L}+$" "héllo"))`] + ["p{L} rejects digits" "false" `(boolean (re-matches #"^\p{L}+$" "ab1"))`] + ["p{N}" "(quote (\"12\" \"345\"))" `(re-seq #"\p{N}+" "a12b345")`] + ["P{N} negation" "\"abc\"" `(re-matches #"^\P{N}+$" "abc")`] + ["inside class" "\"a-1_b\"" `(re-matches #"^[\p{N}\p{L}_-]+$" "a-1_b")`] + ["p{Lu}/p{Ll}" "\"aB\"" `(re-matches #"^\p{Ll}\p{Lu}$" "aB")`] + ["p{Z} space" "\" \"" `(re-matches #"(?u)^[\s\p{Z}]+$" " ")`] + ["p{Ps}/p{Pe}" "\"(x)\"" `(re-matches #"^\p{Ps}x\p{Pe}$" "(x)")`] + ["(?u) accepted" "\"hi\"" `(re-matches #"(?u)^hi$" "hi")`] + ["unknown class throws" :throws `(re-pattern "\p{Greek}")`]) diff --git a/test/spec/untested-vars-spec.janet b/test/spec/untested-vars-spec.janet index 444e184..db2991c 100644 --- a/test/spec/untested-vars-spec.janet +++ b/test/spec/untested-vars-spec.janet @@ -129,8 +129,9 @@ ["delay? true" "true" "(delay? (delay 1))"] ["delay? false" "false" "(delay? 1)"] ["future-call" "42" "(deref (future-call (fn [] 42)))"] - [". member resolves nil" "nil" "(. \"abc\" count)"] - [".. threads members" "nil" "(.. \"abc\" count)"]) + [". calls String surface" "3" "(. \"abc\" length)"] + [".. threads members" "\"ABC\"" "(.. \"abc\" toUpperCase)"] + ["unknown String member throws" :throws "(. \"abc\" frobnicate)"]) (defspec "untested / protocols: extend + extends?" ["extend registers" ":str"