|
1 | 1 | (ns schema.macros |
2 | 2 | "Macros and macro helpers used in schema.core." |
| 3 | + (:refer-clojure :exclude [simple-symbol?]) |
3 | 4 | (:require |
4 | 5 | [clojure.string :as str] |
5 | 6 | [schema.utils :as utils])) |
6 | 7 |
|
| 8 | +;; can remove this once we drop Clojure 1.8 support |
| 9 | +(defn- simple-symbol? [x] |
| 10 | + (and (symbol? x) |
| 11 | + (not (namespace x)))) |
| 12 | + |
7 | 13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
8 | 14 | ;;; Helpers used in schema.core. |
9 | 15 |
|
|
18 | 24 | [then else] |
19 | 25 | (if (cljs-env? &env) then else)) |
20 | 26 |
|
21 | | -(let [bb? (boolean (System/getProperty "babashka.version"))] |
22 | | - (defmacro if-bb |
23 | | - [then else] |
24 | | - (if bb? then else))) |
| 27 | +(def bb? (boolean (System/getProperty "babashka.version"))) |
| 28 | + |
| 29 | +(defmacro if-bb |
| 30 | + [then else] |
| 31 | + (if bb? then else)) |
25 | 32 |
|
26 | 33 | (defmacro try-catchall |
27 | 34 | "A cross-platform variant of try-catch that catches all* exceptions. |
|
406 | 413 | (new ~(symbol (str name)) |
407 | 414 | ~@(map (fn [s] `(safe-get ~map-sym ~(keyword s))) field-schema))))))) |
408 | 415 |
|
| 416 | +(if-bb nil |
| 417 | +(defn -instrument-protocol-method |
| 418 | + "Given a protocol Var pvar, its method method-var and instrument-method, |
| 419 | + instrument the protocol method." |
| 420 | + [pvar ;:- Var |
| 421 | + method-var ;:- (Var InnerMth) |
| 422 | + instrument-method #_:- #_(s/=>* OuterMth |
| 423 | + [InnerMth |
| 424 | + (named (=> Any OuterMth InnerMth) |
| 425 | + 'sync!)])] |
| 426 | + (let [;; propagate method cache to inner method. |
| 427 | + ;; explanation: all functions in Clojure have special support for protocol methods |
| 428 | + ;; via the __methodImplCache field: https://github.com/clojure/clojure/search?q=methodimplcache&type=. |
| 429 | + ;; this mutable field is used inside each protocol method's implementation via (fn this [..] (.__methodImplCache this)) |
| 430 | + ;; and also mutated from the "outside" via (set! .__methodImplCache protocol-method). |
| 431 | + ;; since we wrap protocol methods, we need to preserve these two features (settable from outside, readable from inside). |
| 432 | + sync! (fn [^clojure.lang.AFunction outer-mth |
| 433 | + ^clojure.lang.AFunction inner-mth] |
| 434 | + (when-not (identical? (.__methodImplCache outer-mth) |
| 435 | + (.__methodImplCache inner-mth)) |
| 436 | + ;; lock to prevent outdated outer caches from overwriting newer inner caches |
| 437 | + (locking inner-mth |
| 438 | + (set! (.__methodImplCache inner-mth) |
| 439 | + ;; vv WARNING: must be calculated within protected area |
| 440 | + (.__methodImplCache outer-mth) |
| 441 | + ;; ^^ WARNING: must be calculated within protected area |
| 442 | + )))) |
| 443 | + ^clojure.lang.AFunction inner-mth @method-var |
| 444 | + ^clojure.lang.AFunction outer-mth (instrument-method inner-mth sync!) |
| 445 | + ;; populate outer cache so we can use outer-mth as the protocol method without needing |
| 446 | + ;; to call -reset-methods. |
| 447 | + _ (set! (.__methodImplCache outer-mth) |
| 448 | + (.__methodImplCache inner-mth)) |
| 449 | + method-builder (fn [cache] |
| 450 | + (set! (.__methodImplCache outer-mth) cache) |
| 451 | + (sync! outer-mth inner-mth) |
| 452 | + ;; preempt future fix for CLJ-1796--have a canonical method |
| 453 | + ;; representation for the duration of the protocol, matching |
| 454 | + ;; CLJS semantics. |
| 455 | + outer-mth) |
| 456 | + this-nsym (ns-name *ns*)] |
| 457 | + ;; instrument method builder |
| 458 | + (alter-var-root pvar assoc-in [:method-builders method-var] method-builder) |
| 459 | + ;; defeat Compiler.java inlining capabilities so we can always enforce schemas |
| 460 | + (alter-meta! method-var assoc :inline (fn [& args] |
| 461 | + `((do ~(symbol (name this-nsym) (str (.sym ^clojure.lang.Var method-var)))) |
| 462 | + ~@args))) |
| 463 | + ;; instrument the actual method |
| 464 | + (alter-var-root method-var (fn [_] outer-mth))))) |
| 465 | + |
| 466 | +(defn parse-defprotocol-sig [env pname name+sig+doc] |
| 467 | + (let [[doc name+sig] (let [lst (last name+sig+doc)] |
| 468 | + (if (string? lst) |
| 469 | + [lst (butlast name+sig+doc)] |
| 470 | + [nil name+sig+doc])) |
| 471 | + [method-name sig] (maybe-split-first simple-symbol? name+sig) |
| 472 | + _ (assert! (simple-symbol? method-name) "Missing method name %s" (pr-str method-name)) |
| 473 | + [output-schema sig] (let [fst (first sig)] |
| 474 | + (if (= :- fst) |
| 475 | + (let [nxt (next sig)] |
| 476 | + (assert! nxt "Missing schema after :- in %s" method-name) |
| 477 | + [(first nxt) (next nxt)]) |
| 478 | + [`schema.core/Any sig])) |
| 479 | + _ (assert (seq sig)) |
| 480 | + binds (mapv #(process-arrow-schematized-args env %) |
| 481 | + sig) |
| 482 | + cljs? (cljs-env? env)] |
| 483 | + {:sig (->> (concat (cons method-name binds) (when doc [doc])) |
| 484 | + ;; work around https://clojure.atlassian.net/browse/CLJS-3211 |
| 485 | + (apply list)) |
| 486 | + :method-name method-name |
| 487 | + :schema-form `(schema.core/=>* ~output-schema ~@(map #(mapv (comp :schema meta) %) binds)) |
| 488 | + :instrument-method (let [outer-mth-meta (-> (or (meta method-name) {}) |
| 489 | + (dissoc :always-validate :never-validate) |
| 490 | + (into |
| 491 | + (cond |
| 492 | + (-> method-name meta :never-validate) {:never-validate true} |
| 493 | + (-> method-name meta :always-validate) {:always-validate true} |
| 494 | + (-> pname meta :never-validate) {:never-validate true} |
| 495 | + (-> pname meta :always-validate) {:always-validate true})) |
| 496 | + not-empty) |
| 497 | + inner-mth (gensym) |
| 498 | + gen-binder (fn [gs bind] |
| 499 | + (vec (mapcat #(list %1 :- (-> %2 meta :schema)) gs bind))) |
| 500 | + gen-bind-syms (fn [bind] |
| 501 | + (mapv (fn [s] |
| 502 | + (if (symbol? s) |
| 503 | + (gensym (str (name s) "__")) |
| 504 | + (gensym))) |
| 505 | + bind))] |
| 506 | + (cond |
| 507 | + ;; instrumentation not possible babashka yet |
| 508 | + bb? nil |
| 509 | + |
| 510 | + cljs? |
| 511 | + (let [cljs-nsym (-> env :ns :name) |
| 512 | + ->arity-sym #(symbol (str cljs-nsym "." method-name ".cljs$core$IFn$_invoke$arity$" %)) |
| 513 | + arities (into {} |
| 514 | + (map (fn [bind] |
| 515 | + [(count bind) (gensym)]) |
| 516 | + binds))] |
| 517 | + `(let ~(vec (mapcat (fn [[i g]] |
| 518 | + [g (if (= 1 (count arities)) |
| 519 | + ;; just one arity, wrap method-name |
| 520 | + method-name |
| 521 | + ;; multiple arites, wrap each arity individually. don't save/call old method-name |
| 522 | + ;; as it will dispatch right back to the wrapper's arities in an infinite loop. |
| 523 | + (->arity-sym i))]) |
| 524 | + arities)) |
| 525 | + ;; use defn instead of set! to completely hide the $arity$ methods of the underlying protocol |
| 526 | + ;; in case the cljs compiler attempts inlining. |
| 527 | + (schema.core/defn ~(with-meta method-name |
| 528 | + (assoc outer-mth-meta |
| 529 | + :protocol (symbol (name cljs-nsym) (name pname)) |
| 530 | + :doc doc)) |
| 531 | + :- ~output-schema |
| 532 | + ~@(map (fn [bind] |
| 533 | + (let [arity (count bind) |
| 534 | + gs (gen-bind-syms bind) |
| 535 | + inner-mth (get arities arity) |
| 536 | + _ (assert inner-mth)] |
| 537 | + (list (gen-binder gs bind) |
| 538 | + (cons inner-mth gs)))) |
| 539 | + binds)))) |
| 540 | + :else |
| 541 | + (let [outer-mth (gensym (str method-name "__")) |
| 542 | + sync! (gensym)] |
| 543 | + `(-instrument-protocol-method |
| 544 | + (var ~pname) |
| 545 | + (var ~method-name) |
| 546 | + ;; a function that wraps a protocol method in a schema check with a |
| 547 | + ;; cache synchronization point |
| 548 | + (fn [~inner-mth ~sync!] |
| 549 | + (schema.core/fn ~(with-meta outer-mth outer-mth-meta) |
| 550 | + :- ~output-schema |
| 551 | + ~@(map (fn [bind] |
| 552 | + (let [gs (gen-bind-syms bind)] |
| 553 | + (list (gen-binder gs bind) |
| 554 | + (list sync! outer-mth inner-mth) |
| 555 | + (cons inner-mth gs)))) |
| 556 | + binds)))))))})) |
| 557 | + |
| 558 | +(defn process-defprotocol [env name+opts+sigs] |
| 559 | + (let [[pname opts+sigs] (maybe-split-first simple-symbol? name+opts+sigs) |
| 560 | + _ (assert! (simple-symbol? pname) "Missing protocol name: %s" (pr-str pname)) |
| 561 | + [doc opts+sigs] (maybe-split-first string? opts+sigs) |
| 562 | + [opts sigs] (loop [preamble [] |
| 563 | + [fst :as opts+sigs] opts+sigs] |
| 564 | + (if (keyword? fst) |
| 565 | + (let [nxt (next opts+sigs)] |
| 566 | + (assert! nxt "Uneven args to defprotocol %s" pname) |
| 567 | + (recur (conj preamble fst (first nxt)) |
| 568 | + (next nxt))) |
| 569 | + [preamble opts+sigs]))] |
| 570 | + {:pname pname |
| 571 | + :opts opts |
| 572 | + :doc doc |
| 573 | + :parsed-sigs (mapv (partial parse-defprotocol-sig env pname) sigs)})) |
| 574 | + |
409 | 575 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
410 | 576 | ;;; Public: helpers for schematized functions |
411 | 577 |
|
|
0 commit comments