Created
April 13, 2021 08:54
-
-
Save borkdude/9390e0d450ca61d42a037c969d646d9b to your computer and use it in GitHub Desktop.
JPoint 2021 REPL session
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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