;;; ob-julia --- Org Mode babel support for julia, using ESS ;;; Commentary: ;; This package adds Julia support to Org Mode src block evaluation ;;; Code: (require 'ob) (require 'seq) (eval-when-compile (require 'cl)) (defcustom org-babel-julia-command "julia" "Name of command to use for executing julia code." :group 'org-babel :version "24.1" :type 'string) (defcustom ob-julia-startup-script (concat (file-name-directory (or load-file-name (buffer-file-name))) "init.jl") "Julia file path to run at startup. Must be absolute." :group 'org-babel :version "24.1" :type 'string) (defcustom org-babel-julia-table-as-dict nil "If t, tables are imported as Dictionary, else as NamedTuple. In both cases, if you use DataFrames you can pass them to `DataFrame'. Importing NamedTuple is slower (more data) but they preserve the column order." :group 'org-babel :version "24.1" :type 'boolean) (defcustom org-babel-julia-silent-repl nil "Disable printing results in julia REPL. When non-nil, do not print org-src evaluation result in julia session REPL. Since printing results require extra compuatations, if you never look at the REPL setting this non-nil this might be desired. There's no effect in non-session evaluations" :group 'org-babel :version "24.1" :type 'boolean) (defcustom org-babel-julia-debug nil "Enable sending messages with debugging information." :group 'org-babel :version "24.1" :type 'boolean) (defconst org-babel-header-args:julia '((width . :any) (height . :any) (size . :any) (inline . :any) (let . :any) (import . :any) (using . :any) (async . :any) (results . ((file matrix table list ;; vector table scalar verbatim ) (raw html latex org ;; code pp drawer ) (replace silent none append prepend) (output value)))) "Julia-specific header arguments.") (defvar org-babel-default-header-args:julia '()) (defvar org-babel-julia-default-session "*julia*") (defvar ess-ask-for-ess-directory nil) ; dynamically scoped (defvar org-babel-julia-session-directory) (defun org-babel-prep-session:julia (session params) "Prepare SESSION according to the header arguments specified in PARAMS." (let ((dir (or (cdr (assoc :dir params)) (inferior-ess--maybe-prompt-startup-directory org-babel-julia-command "julia")))) (set (make-local-variable 'org-babel-julia-session-directory) dir) (save-window-excursion (require 'ess) (require 'ess-julia) ;; load the julia startup script (defined in ob-julia-startup-script) ;; pass it along with other arguments defined in inferior-julia-args (let* ((start-script-arg (concat (format "--load=%s" ob-julia-startup-script))) (inferior-julia-args (if inferior-julia-args (concat inferior-julia-args start-script-arg) start-script-arg))) (switch-to-buffer (julia))) (rename-buffer (if (bufferp session) (buffer-name session) (if (stringp session) session (buffer-name)))) ;; Register the async callback. Important to do this before ;; running the command (set-process-filter (get-buffer-process (org-babel-comint-buffer-livep session)) 'org-julia-async-process-filter)))) (defun org-babel-julia-get-session-name (params) "Extract the session name from the PARAMS. If session should not be used, return nil. session can be: - (:session) :: param passed, empty, use default - (:session name) :: param passed, with a name, use it - (:session none) :: param not passed, do not use the session" (let ((session (cdr (assoc :session params)))) (cond ((null session) org-babel-julia-default-session) ((string-equal session "none") nil) (t session)))) (defun org-julia-async-process-filter (process output) "Replace julia-async: tags with async results. Takes OUTPUT from PROCESS, tries to extract from the ob_julia_async the `uuid' in the `org-mode' buffer name. Then, searches for the `uuid' in the `org-mode' buffer, and replaces it with the output file content. This function is used for all async processing with and without session." (if (string-match "ob_julia_async_\\([0-9a-z\\-]+\\)_\\(.+\\)" output) ;; capture ob-julia ouptut (progn (let ((uuid (match-string-no-properties 1 output)) (org-buffer (match-string-no-properties 2 output)) new-hash results params cache info) (save-window-excursion (save-excursion (switch-to-buffer org-buffer) (save-restriction ;; If it's narrowed, substitution would fail (widen) ;; search the matching src block (goto-char (point-max)) (when (search-backward (concat "julia-async:" uuid) nil t) ;; get output file name (stored in the buffer (setq results (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) (when (string-match "julia-async:.+:\\([^\s]*\\)" line) (match-string-no-properties 1 line)))) ;; remove results (search-backward "#+end_src") (setq info (org-babel-get-src-block-info 'light)) ;; This will evaluate the code again ;; (cl-callf org-babel-process-params (nth 2 info)) (setq params (nth 2 info)) (setq cache (ob-julia-check-trueness params :cache)) ;; pass info to have a different hash (setq new-hash (if cache (org-babel-sha1-hash) nil)) (org-babel-remove-result) ;; insert new one (org-babel-insert-result (org-babel-julia-process-results results params 'callback) (cdr (assq :result-params params)) info new-hash "julia"))))) (inferior-ess-output-filter process "\n"))) ;; This is the standard (inferior-ess-output-filter process output))) (defun org-babel-julia-evaluate-external-process (block outfile params buffer) "Evaluate julia SRC code, according to PARAMS. Does not rely on an ESS session." (let* ((uuid (org-id-uuid)) (command (format "%s;println(string(\"ob_julia_async_\", %S, \"_\", %S))" block uuid buffer)) (tmpfile (make-temp-file "ob-julia" nil ".jl" block))) (if (and (org-babel-julia-async-p params) (org-babel-julia-really-async-p)) (progn (make-process :name "*julia-async-process*" :filter #'org-julia-async-process-filter :command `(,org-babel-julia-command "--load" ,ob-julia-startup-script "--eval" ,(format "include(%S);%s" tmpfile command))) (concat "julia-async:" uuid ":" outfile)) (progn (shell-command (format "%s --load %s %s" org-babel-julia-command ob-julia-startup-script tmpfile)) outfile)))) (defun org-babel-julia-assign-to-var-or-array (var) "" (if (listp (cdr var)) (org-babel-julia-assign-to-array (car var) (cdr var)) (org-babel-julia-assign-to-var (car var) (cdr var)))) (defun org-babel-julia-assign-to-array (name matrix) "Create a Matrix (Vector{Any,2} from `MATRIX' and assign it to `NAME'" (format "%s = [%s]" name (mapconcat (lambda (line) (mapconcat (lambda (e) (format "%S" e)) line " ")) matrix ";"))) (defun org-babel-julia-assign-to-var (name value) "Assign `VALUE' to a variable called `NAME'." (format "%s = %S" name value)) (defun org-babel-julia-assign-to-dict (name column-names values) "Create a Dict with lists as values. Create a Dict where keys are Symbol from `COLUMN-NAMES', values are Array taken from `VALUES', and assign it to `NAME'" (format "%s = Dict(%s)" name (mapconcat (lambda (i) (format "Symbol(\"%s\") => [%s]" (nth i column-names) (mapconcat (lambda (line) (format "%S" (nth i line))) values ","))) (number-sequence 0 (1- (length column-names))) ","))) (defun org-babel-julia-assign-to-named-tuple (name column-names values) "Create a NamedTuple using (; zip([], [])...)" (let ((res (format "%s = [%s]" name (mapconcat (lambda (i) (concat "(; zip([" (mapconcat (lambda (col) (format "Symbol(\"%s\")" col)) column-names ", ") "],[" (mapconcat (lambda (cell) (format "\"%s\"" cell)) (nth i values) ",") "])...)")) (number-sequence 0 (1- (length values))) ", ")))) (message res) res)) (defun org-babel-variable-assignments:julia (params) "Return list of julia statements assigning the block's variables." (let ((vars (org-babel--get-vars params)) (colnames (cdr (assoc :colname-names params)))) (mapcar (lambda (i) (let* ((var (nth i vars)) (column-names (car (seq-filter (lambda (cols) (eq (car cols) (car var))) colnames)))) (if column-names (if org-babel-julia-table-as-dict (org-babel-julia-assign-to-dict (car var) (cdr column-names) (cdr var)) (org-babel-julia-assign-to-named-tuple (car var) (cdr column-names) (cdr var))) (org-babel-julia-assign-to-var-or-array var)))) (number-sequence 0 (1- (length vars)))))) (defun org-babel-julia-make-kwargs (args) "" (format "(%s)" (mapconcat (lambda (arg) (format "%s=%s," (car arg) (cdr arg))) (seq-filter (lambda (arg) (cdr arg)) args) ""))) (defun org-babel-julia-block-expand (params srcfile outfile) "Takes BODY, apply required PARAMS and return the Julia code. OUTFILE and FILE can either be a string or nil. If FILE is defined, output is _save()_d to a file with that name. else OUTFILE is used, and data is _write()_ to it." (let* ((vars (mapconcat 'concat (org-babel-variable-assignments:julia params) ";")) (varsfile (make-temp-file "ob-julia-vars-" nil ".jl" vars)) (dir (or (cdr (assoc :dir params)) default-directory)) (using-param (cdr (assoc :using params))) (using (if using-param (split-string using-param) nil)) (import-param (cdr (assoc :import params))) (import (if import-param (split-string import-param ";") nil)) (result-type (cdr (assoc :result-type params))) (output-type (case result-type (value ":value") (output ":output"))) ;; kwargs (size (cdr (assoc :size params))) (width (cdr (assoc :width params))) (height (cdr (assoc :height params)))) (concat (if (or using import) (format "OrgBabelImport(%S);OrgBabelReload();" (concat (if using (mapconcat (lambda (x) (concat "using " x)) using "\n") "") "\n" (if import (mapconcat (lambda (x) (concat "import " x)) import "\n") ""))) "") (format "OrgBabelFormat(%s,%S,%S,%S,%S,%s,%s,%S);" output-type outfile dir varsfile srcfile (if org-babel-julia-silent-repl "true" "false") (if (ob-julia-check-trueness params :let) "true" "false") (org-babel-julia-make-kwargs `((width . ,width) (height . ,height) (size . ,size))))))) (defun org-babel-execute:julia-async (buffer session body block output params) (let* ((uuid (org-id-uuid)) ;; The whole line must be printed in as single statement ;; (ob_julia_async...) or you can receive only a portion of ;; it. But cannot be joined together (else it will trigger ;; immediately). That's why I'm using string(..) (command (format "%s;println(string(\"ob_julia_async_\", %S, \"_\", %S))" block uuid buffer))) (progn (org-babel-remove-result) (process-send-string session (concat command "\n")) ;; store the command in input history! (with-current-buffer session (comint-add-to-input-history body))) (concat "julia-async:" uuid ":" output))) (defun org-babel-execute:julia-sync (session body block output params) "Run FILE, in session `SESSION`, synchronously. PARAMS are passed" (org-babel-comint-eval-invisibly-and-wait-for-file session output block 0.1) (with-current-buffer session (comint-add-to-input-history body)) output) (defun org-babel-julia-process-value-result (results type) "Insert hline if needed (combining info from RESULT and TYPE." ;; add an hline if the result seems to be a table ;; always obay explicit type (if (or (eq type 'table) (and (eq type 'auto) (listp results) ; a table must be a list (listp (car results)) ; of lists (stringp (caar results)))) ; with strings as first line (cons (car results) (cons 'hline (cdr results))) results)) (defun org-babel-julia-process-results (results params &optional callback) "Decides what to insert as result. If PARAMS is :async, insert a link, unless CALLBACK is true." (let ((result-type (org-babel-julia-parse-result-type params)) (file (cdr (assoc :file params))) (inlined (org-babel-julia-get-inline-type params)) (async (org-babel-julia-async-p params)) (session (org-babel-julia-get-session-name params)) (res (cdr (assoc :results params)))) (if (and async (not callback) (org-babel-julia-really-async-p)) results (unless file ; do not process files (when org-babel-julia-debug (message (format "Processing results %s" results))) (if inlined (with-temp-buffer (when (bound-and-true-p org-export-current-backend) (insert (format "#+begin_export %s\n" (if org-export-current-backend org-export-current-backend inlined)))) (insert-file-contents results) (when (bound-and-true-p org-export-current-backend) (goto-char (point-max)) (insert "\n#+end_export")) (buffer-string)) (org-babel-result-cond (if res (split-string res) nil) (with-temp-buffer (when org-babel-julia-debug (message res)) (insert-file-contents results) (buffer-string)) (org-babel-julia-process-value-result (org-babel-import-elisp-from-file results '(4)) result-type))))))) (defun org-babel-julia-parse-result-type (params) "Decide how to parse results. Default is \"auto\" (results can be anything. If \"table\", force parsing as a table. To force a matrix, use matrix" (let* ((results (cdr (assoc :results params))) (results (if (stringp results) (split-string results) nil))) (cond ((member "table" results) 'table) ((member "matrix" results) 'matrix) ((member "raw" results) 'raw) (t 'auto)))) (defun ob-julia-check-trueness (params param) "" (and (assoc param params) (let ((val (cdr (assoc param params)))) (or (not val) (string= "t" val) (string= "yes" val))))) (defun org-babel-julia-async-p (params) "Check whether the session should be async or not." (let* ((res (cdr (assoc :results params))) (async (assoc :async params))) (and async (ob-julia-check-trueness params :async) (and (eq org-babel-current-src-block-location (org-babel-where-is-src-block-head))) (not (and res (stringp res) (member "silent" (split-string res))))))) (defun org-babel-julia-really-async-p () (not (bound-and-true-p org-export-current-backend))) ;; Copied from ob-python (defun org-babel-julia-with-earmuffs (session) (let ((name (if (stringp session) session (format "%s" session)))) (if (and (string= "*" (substring name 0 1)) (string= "*" (substring name (- (length name) 1)))) name (format "*%s*" name)))) (defun org-babel-julia-get-inline-type (params) "Parse the :inline header from PARAMS. Returns t, nil or the output format." (let ((inlined (assoc :inline params))) (if inlined (if (and (cdr inlined) (not (string= (cdr inlined) "no"))) (cdr inlined) (if (bound-and-true-p org-export-current-backend) (format "%s" org-export-current-backend) nil)) nil))) (defun org-babel-execute:julia (body params) "Execute a block of julia code. This function is called by `org-babel-execute-src-block'. BODY is the content of the src block PARAMS are the parameter passed to the block" ;; org-babel-current-src-block-location ; this variable does not work >.< (save-excursion (let* ((buffer (buffer-name)) (session (org-babel-julia-get-session-name params)) (async (org-babel-julia-async-p params)) (file (cdr (assoc :file params))) (inlined (org-babel-julia-get-inline-type params)) (outfile (org-babel-process-file-name (if file (concat default-directory file) (org-babel-temp-file "julia-" (if inlined (format ".%s" inlined) ""))))) (src (make-temp-file "ob-julia" nil ".jl" body)) (block (org-babel-julia-block-expand params src outfile))) (when org-babel-julia-debug (message block)) (if session (progn ;; TODO: check if session exists, if it does, make it like ;; *session:$N* (where N is the first number available) (setq session (org-babel-julia-with-earmuffs session)) (when (not (org-babel-comint-buffer-livep session)) (org-babel-prep-session:julia session params)) (if (and async (org-babel-julia-really-async-p)) (progn (when org-babel-julia-debug (message "async export")) (org-babel-julia-process-results (org-babel-execute:julia-async buffer session body block outfile params) params)) (progn (when org-babel-julia-debug (message "sync export")) (org-babel-julia-process-results (org-babel-execute:julia-sync session body block outfile params) params)))) (let ((res (org-babel-julia-evaluate-external-process block outfile params buffer))) (if (and async (org-babel-julia-really-async-p)) res (org-babel-julia-process-results res params))))))) (add-to-list 'org-babel-tangle-lang-exts '("julia" . "jl")) (provide 'ob-julia) ;;; ob-julia.el ends here