Skip to content

Instantly share code, notes, and snippets.

@borkdude
Created April 13, 2021 08:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save borkdude/9390e0d450ca61d42a037c969d646d9b to your computer and use it in GitHub Desktop.
Save borkdude/9390e0d450ca61d42a037c969d646d9b to your computer and use it in GitHub Desktop.
JPoint 2021 REPL session
(ns jpoint.repl
(:require [babashka.main :as bb]
[cheshire.core :as json]
[edamame.core :as e]
[sci.core :as sci]
[sci.impl.analyzer :as ana]
[sci.impl.evaluator :as eval]
[sci.impl.interop :as interop]
[sci.impl.parser :as p]))
;; Notes
(comment
;; REPL:
;; In babashka project: "lein with-profiles +test do clean, repl"
;; Useful for hiding/showing blocks:
;; hs-minor-mode, hs-show-block, hs-hide-block
)
;; Intro
(comment
;; See slides
)
;; Clojure intro
(comment
;; calling a function
(+ 10 20)
;; defining a function
(defn foo [a b]
(+ a b))
(foo 10 20)
;; calling Java constructor:
(String. "foo")
;; static method:
(java.util.Base64/getEncoder)
;; instance method:
(.getBytes "foo")
;; all together:
(String. (.encode (java.util.Base64/getEncoder) (.getBytes "foo")))
;; clojure var
(def f (fn [] :foo))
(f) ;;=> :foo
(var? #'f) ;; true
;; vars can be re-defined:
(def f (fn [] :bar))
(f) ;;=> :bar
;; or using alter-var-root:
(alter-var-root #'f (constantly (fn [] :baz)))
(f) ;;=> :baz
(defmacro my-macro [& body]
`(do (println :foo) ~@body))
(macroexpand '(my-macro (+ 1 2 3)))
;; (do (clojure.core/println :foo) (+ 1 2 3))
(my-macro (+ 1 2 3))
;; metadata
(def x ^{:foo true} [])
(meta x) ;; {:foo true}
,)
;; Usage of bb and sci API
(comment
;; Normally we call bb as a CLI app, but today we will look at the
;; internals from within a REPL.
;;;; Exit code ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; (require '[babashka.main :as bb])
(bb/main "-e" "(+ 1 2 3)") ;;=> 0, the exit code
(with-out-str (bb/main "(+ 1 2 3)")) ;;=> "6\n", printed to stdout
;; Babashka uses sci to interpret code
;; (require '[sci.core :as sci])
(sci/eval-string "(+ 1 2 3)") ;;=> 6
(bb/main "(/ 1 0)") ;;=> 1, non-zero exit code
(with-out-str
(binding [*err* *out*]
(bb/main "(/ 1 0)"))) ;;=> Divide by zero
;; Whoops, exception:
(sci/eval-string "(/ 1 0)")
;;;; Interop ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Babashka can do interop
(with-out-str (bb/main "(java.io.File. \".\")")) ;;=> "#object[java.io.File 0x70b70ff5 \".\"]\n"
(sci/eval-string "(java.io.File. \".\")") ;; unable to resolve classname java.io.File
;; We need to explicitly add classes. This works:
(sci/eval-string "(java.io.File. \".\")" {:classes {'java.io.File java.io.File}})
;;;; Libraries ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Babashka has libs built in
(with-out-str (bb/main "
(require '[cheshire.core :as json])
(json/parse-string (json/generate-string {:a 1}) true)")) ;;=> "{:a 1}\n"
(sci/eval-string "
(require '[cheshire.core :as json])
(json/parse-string (json/generate-string {:a 1}) true)") ;;=> Could not find namespace cheshire.core
;; (require '[cheshire.core :as json])
;; This works:
(sci/eval-string "
(require '[cheshire.core :as json])
(json/parse-string (json/generate-string {:a 1}) true)"
{:namespaces {'cheshire.core {'generate-string json/generate-string
'parse-string json/parse-string}}})
;;;;; Vars ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sci/eval-string "user/x"
{:namespaces {'user {'x 1}}}) ;;=> 1
;; This doesn't work, because 1 isn't a var in sci
(sci/eval-string "(alter-var-root #'user/x (constantly 2))"
{:namespaces {'user {'x 1}}})
;; This does work because def creates a sci var
(sci/eval-string "
(def x 1)
(alter-var-root #'user/x (constantly 2))") ;;=> 2
;; How do we create sci vars to pass via opts?
(def x (sci/new-var 'x 1))
(sci/eval-string "(alter-var-root #'user/x (constantly 2))"
{:namespaces {'user {'x x}}}) ;;=> 2
;; What about macros?
(sci/with-out-str (sci/eval-string "(defmacro foo [x] `(do ~x ~x)) (foo (prn :x))"))
;;=> ":x\n:x\n"
(sci/with-out-str (sci/eval-string "(foo (prn :x))"
{:namespaces {'user {'foo (fn [x] `(do ~x ~x))}}}))
;;=> ":x\n"
;; Hmm, only one :x, why?
;; Metadata to mark fn as macro:
(sci/with-out-str (sci/eval-string
"(foo (prn :x))"
{:namespaces {'user {'foo ^:sci/macro (fn [_form _env x]
;; inspect form!
;; (prn _form)
`(do ~x ~x))}}}))
(sci/eval-string "(defmacro foo [x] `)")
;; Create a macro contained in sci var:
(def foo (sci/new-macro-var 'foo (fn [_form _env x]
;; inspect form!
;; (prn _form)
`(do ~x ~x))))
(sci/with-out-str (sci/eval-string
"(foo (prn :x))"
{:namespaces {'user {'foo foo}}}))
;; Copy existing functions and macros into sci vars:
(defmacro foo* [x] `(do ~x ~x))
(macroexpand '(foo* (prn :x))) ;; => (do (prn :x) (prn :x))
;; Let's copy using sci/copy-var, which needs a sci ns.
(def user-ns (sci/create-ns 'user))
(def sci-macro-var (sci/copy-var foo* user-ns))
(sci/with-out-str (sci/eval-string
"(foo (prn :x))"
{:namespaces {'user {'foo sci-macro-var}}}))
;;=> ":x\n:x\n"
::fin)
;; Creating a sci-based REPL
(comment
;; In a REPL we need to remember context as we evaluate form after form.
(def ctx (sci/init {}))
(sci/eval-string* ctx "(def x 1)")
(sci/eval-string* ctx "x") ;; => 1
;; Collecting input
(sci/eval-string (read-line))
;; Read-line is not enough: keep reading until form is complete.
(def rdr (sci/reader *in*))
;; Parsing a complete form from stdin:
(sci/parse-next ctx rdr)
(with-in-str "(+ 1 2\n\n3)" (sci/parse-next ctx (sci/reader *in*))) ;; ;;=> (+ 1 2 3)
;; How to evaluate?
(sci/eval-form ctx '(+ 1 2 3)) ;;=> 6
;; All together now:
(with-in-str "(+ 1 2\n\n3)" (->> (sci/reader *in*)
(sci/parse-next ctx )
(sci/eval-form ctx))) ;; => 6
(defn repl []
(let [next-form (sci/parse-next ctx (sci/reader *in*))
next-val (sci/eval-form ctx next-form)]
(when-not (identical? :repl/quit next-val)
(prn next-form '-> next-val)
(repl))))
(repl)
)
;; Parsing forms
(comment
;; Edamame is the lib that sci uses to parse Clojure forms
;; It is GraalVM-compatible (no eval) and attaches location metadata to forms
(e/parse-string "{:a 1}") ;;=> {:a 1}
(meta (e/parse-string "{:a 1}")) ;;=> {:row 1, :col 1, :end-row 1, :end-col 7}
;; Resolving namespaces
(e/parse-string "::foo") ;; ERROR, should use :auto-resolve + :current to resolve ns
(e/parse-string "::foo" {:auto-resolve {:current 'foo}}) ;;=> :foo/foo
(e/parse-string "::s/foo" {:auto-resolve {'s 'clojure.string}}) ;;=> clojure.string/foo
;; Reader conditionals
(e/parse-string "#?(:bb 1 :clj 2)" {:read-cond true}) ;;=> nil
(e/parse-string "#?(:bb 1 :clj 2)" {:read-cond true :features #{:bb}}) ;;=> 1
;; Syntax quote
(eval (e/parse-string "`[1 2 x]" {:syntax-quote true})) ;; [1 2 x]
(eval (e/parse-string "`[1 2 x]"
{:syntax-quote {:resolve-symbol (fn [_] 'foobar/x)}}))
;; => [1 2 foobar/x]
;; So this is basically what is going on in sci.impl.parser
::fin)
;; Inside sci
(comment
;; (require '[sci.impl.parser :as p])
;; (require '[sci.impl.analyzer :as ana])
;; (require '[sci.impl.evaluator :as eval])
#_{:clj-kondo/ignore [:redefined-var]}
(def ctx (sci/init {}))
;; The analyzer's job is to inspect and enrich forms with instructions for the
;; interpreter
(ana/analyze ctx (sci/parse-string ctx "x")) ;; could not resolve symbol x at
;; line 1, column 1...
(def analyzed-fn (ana/analyze ctx (p/parse-string ctx "(fn [] 1)")))
(meta analyzed-fn) ;;=> :sci.impl/op :fn
;; sci.impl/op decides what the interpreter is going to do with the form
(def interpreted-fn (eval/eval ctx analyzed-fn))
(interpreted-fn) ;;=> 1
(def analyzed-def
(ana/analyze ctx (sci/parse-string ctx "(def x 1)"))) ;;=> (def x 1)
;; #object[sci.impl.types.EvalFn 0x270a518a "(def x 1)"]
;; EvalFn is a thing that contains a function (closure) of the ctx and the it
;; evaluates itself
;; This function is produced in the analyzer
(eval/eval ctx analyzed-def) ;; now x is defined...
(require '[sci.impl.utils :as u])
(eval/eval ctx (u/ctx-fn (fn [ctx] :foo)
'(if 1 2 3) ;; expression remembered for debugging
)) ;; :foo
(type (eval/eval ctx analyzed-def)) ;;=> sci.impl.vars.Var
;; deref the var:
(eval/eval ctx (ana/analyze ctx (p/parse-string ctx "x"))) ;;=> 1
(sci/eval-string* ctx "x") ;;=> 1
;; if example
;; see analyzer.cljc, return-if
(eval/eval ctx (ana/analyze ctx '(if 1 2 3))) ;;=> 2
(ana/analyze ctx '(if 1 2 3)) ;;=> 2, optimization in analyzer
(ana/analyze ctx '(if 1)) ;; Too few arguments too if
(ana/analyze ctx '(odd? 1))
(eval/eval ctx (ana/analyze ctx '(if (odd? 1) :foo :bar)))
;; Let's define a macro:
(sci/eval-string* ctx "(defmacro foo [] `(+ 1 2 3))")
;; The analyzer also does macro-expansion:
(def analyzed-expr (ana/analyze ctx (p/parse-string ctx "(foo)")))
(eval/eval ctx analyzed-expr) ;;=> 6
;; Interop
java.io.File/separator
#_{:clj-kondo/ignore [:redefined-var]}
(def ctx (sci/init {:classes {'java.io.File java.io.File}}))
(def analyzed-static-field
(ana/analyze ctx (p/parse-string ctx "java.io.File/separator")))
;;=> [java.io.File separator]
(meta analyzed-static-field) ;;=> :sci.impl/op :static-access
(eval/eval ctx analyzed-static-field) ;;=> "/"
(interop/get-static-field analyzed-static-field) ;;=> "/"
)
;; Misc. topics, time permitting
(comment
;; GraalVM compilation, reflection config
;; Multimethods, protocols, reify
)
;; Local variables:
;; eval: (hs-minor-mode)
;; eval: (hs-hide-all)
;; eval: (setq global-hl-line-mode nil)
;; eval: (setq show-trailing-whitespace nil)
;; End:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment