536 lines
19 KiB
EmacsLisp
536 lines
19 KiB
EmacsLisp
;;; 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 org-babel-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)
|
|
(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)
|
|
(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)
|
|
;; Initialization
|
|
(let ((julia-init
|
|
(with-temp-buffer
|
|
(insert-file-contents org-babel-julia-startup-script)
|
|
(buffer-string))))
|
|
(ess-send-string (ess-get-process) julia-init nil))
|
|
(current-buffer))))
|
|
|
|
(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 (let ((c (cdr (assq :cache params))))
|
|
(and c (string= "yes" c))))
|
|
;; 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" ,org-babel-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
|
|
org-babel-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 ":%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"
|
|
(format "%s = [%s]" name
|
|
(mapconcat
|
|
(lambda (i)
|
|
(concat
|
|
"(" (mapconcat
|
|
(lambda (j)
|
|
(format "%s=%S"
|
|
(nth j column-names)
|
|
(nth j (nth i values))))
|
|
(number-sequence 0 (1- (length column-names)))
|
|
",")
|
|
")"))
|
|
(number-sequence 0 (1- (length values))) ", ")))
|
|
|
|
(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 (org-babel-variable-assignments:julia params))
|
|
(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") "")
|
|
(if import (mapconcat (lambda (x) (concat "import " x))
|
|
import "\n") "")))
|
|
"")
|
|
(format
|
|
"OrgBabelFormat(%s,%S,%S,%S,%S,%s,%S);"
|
|
output-type outfile
|
|
dir
|
|
(mapconcat 'concat vars ";") srcfile
|
|
(if org-babel-julia-silent-repl
|
|
"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 "@@%s:"
|
|
(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 "@@"))
|
|
(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 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
|
|
(or
|
|
(not (cdr async))
|
|
(string= "t" (cdr async))
|
|
(string= "yes" (cdr async)))
|
|
(not (and res (stringp res) (member "silent" (split-string res)))))))
|
|
|
|
(defun org-babel-julia-really-async-p ()
|
|
;; (let*
|
|
;; ((head (org-babel-where-is-src-block-head))
|
|
;; (async (and (not (bound-and-true-p org-export-current-backend))
|
|
;; head
|
|
;; org-babel-current-src-block-location
|
|
;; (equal org-babel-current-src-block-location
|
|
;; head))))
|
|
;; async)
|
|
;; Disable async on export
|
|
(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)))))))
|
|
|
|
(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 >.<
|
|
(message (format "body: %s, params: %s" body params))
|
|
(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
|