ob-julia.el (12666B)
1 ;;; ob-julia.el --- org-babel functions for julia code evaluation -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2013-2024 Free Software Foundation, Inc. 4 ;; Authors: G. Jay Kerns 5 ;; Maintainer: Pedro Bruel <pedro.bruel@gmail.com> 6 ;; Keywords: literate programming, reproducible research, scientific computing 7 ;; URL: https://github.com/phrb/ob-julia 8 9 ;; This file is part of GNU Emacs. 10 11 ;; GNU Emacs is free software: you can redistribute it and/or modify 12 ;; it under the terms of the GNU General Public License as published by 13 ;; the Free Software Foundation, either version 3 of the License, or 14 ;; (at your option) any later version. 15 16 ;; GNU Emacs is distributed in the hope that it will be useful, 17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 ;; GNU General Public License for more details. 20 21 ;; You should have received a copy of the GNU General Public License 22 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 23 24 ;;; Commentary: 25 26 ;; Org-Babel support for evaluating julia code 27 ;; 28 ;; Based on ob-R.el by Eric Schulte and Dan Davison. 29 ;; 30 ;; Session support requires the installation of the DataFrames and CSV 31 ;; Julia packages. 32 33 ;;; Code: 34 35 (require 'org-macs) 36 (org-assert-version) 37 38 (require 'cl-lib) 39 (require 'ob) 40 41 (declare-function orgtbl-to-csv "org-table" (table params)) 42 (declare-function julia "ext:ess-julia" (&optional start-args)) 43 (declare-function inferior-ess-send-input "ext:ess-inf" ()) 44 (declare-function ess-make-buffer-current "ext:ess-inf" ()) 45 (declare-function ess-eval-buffer "ext:ess-inf" (vis)) 46 (declare-function ess-wait-for-process "ext:ess-inf" 47 (&optional proc sec-prompt wait force-redisplay)) 48 49 (defvar org-babel-header-args:julia 50 '((width . :any) 51 (horizontal . :any) 52 (results . ((file list vector table scalar verbatim) 53 (raw org html latex code pp wrap) 54 (replace silent append prepend) 55 (output value graphics)))) 56 "Julia-specific header arguments.") 57 58 (add-to-list 'org-babel-tangle-lang-exts '("julia" . "jl")) 59 60 (defvar org-babel-default-header-args:julia '()) 61 62 (defcustom org-babel-julia-command "julia" 63 "Name of command to use for executing julia code." 64 :version "24.3" 65 :package-version '(Org . "8.0") 66 :group 'org-babel 67 :type 'string) 68 69 (defvar ess-current-process-name) ; dynamically scoped 70 (defvar ess-local-process-name) ; dynamically scoped 71 (defvar ess-eval-visibly-p) ; dynamically scoped 72 (defvar ess-local-customize-alist); dynamically scoped 73 (defvar ess-gen-proc-buffer-name-function) ; defined in ess-inf.el 74 (defun org-babel-julia-associate-session (session) 75 "Associate R code buffer with an R session. 76 Make SESSION be the inferior ESS process associated with the 77 current code buffer." 78 (when-let* ((process (get-buffer-process session))) 79 (setq ess-local-process-name (process-name process)) 80 (ess-make-buffer-current)) 81 (setq-local ess-gen-proc-buffer-name-function (lambda (_) session))) 82 83 (defun org-babel-expand-body:julia (body params &optional _graphics-file) 84 "Expand BODY according to PARAMS, return the expanded body." 85 (mapconcat #'identity 86 (append 87 (when (cdr (assq :prologue params)) 88 (list (cdr (assq :prologue params)))) 89 (org-babel-variable-assignments:julia params) 90 (list body) 91 (when (cdr (assq :epilogue params)) 92 (list (cdr (assq :epilogue params))))) 93 "\n")) 94 95 (defun org-babel-execute:julia (body params) 96 "Execute a block of julia code. 97 This function is called by `org-babel-execute-src-block'." 98 (save-excursion 99 (let* ((result-params (cdr (assq :result-params params))) 100 (result-type (cdr (assq :result-type params))) 101 (session (org-babel-julia-initiate-session 102 (cdr (assq :session params)) params)) 103 (graphics-file (and (member "graphics" (assq :result-params params)) 104 (org-babel-graphical-output-file params))) 105 (colnames-p (unless graphics-file (cdr (assq :colnames params)))) 106 (full-body (org-babel-expand-body:julia body params graphics-file)) 107 (result 108 (org-babel-julia-evaluate 109 session full-body result-type result-params 110 (or (equal "yes" colnames-p) 111 (org-babel-pick-name 112 (cdr (assq :colname-names params)) colnames-p))))) 113 (if graphics-file nil result)))) 114 115 (defun org-babel-normalize-newline (result) 116 (replace-regexp-in-string 117 "\\(\n\r?\\)\\{2,\\}" 118 "\n" 119 result)) 120 121 (defun org-babel-prep-session:julia (session params) 122 "Prepare SESSION according to the header arguments specified in PARAMS." 123 (let* ((session (org-babel-julia-initiate-session session params)) 124 (var-lines (org-babel-variable-assignments:julia params))) 125 (org-babel-comint-in-buffer session 126 (mapc (lambda (var) 127 (end-of-line 1) (insert var) (comint-send-input nil t) 128 (org-babel-comint-wait-for-output session)) var-lines)) 129 session)) 130 131 (defun org-babel-load-session:julia (session body params) 132 "Load BODY into SESSION." 133 (save-window-excursion 134 (let ((buffer (org-babel-prep-session:julia session params))) 135 (with-current-buffer buffer 136 (goto-char (process-mark (get-buffer-process (current-buffer)))) 137 (insert (org-babel-chomp body))) 138 buffer))) 139 140 ;; helper functions 141 142 (defun org-babel-variable-assignments:julia (params) 143 "Return list of julia statements assigning the block's variables." 144 (let ((vars (org-babel--get-vars params))) 145 (mapcar 146 (lambda (pair) (org-babel-julia-assign-elisp (car pair) (cdr pair))) 147 (mapcar 148 (lambda (i) 149 (cons (car (nth i vars)) 150 (org-babel-reassemble-table 151 (cdr (nth i vars)) 152 (cdr (nth i (cdr (assq :colname-names params)))) 153 (cdr (nth i (cdr (assq :rowname-names params))))))) 154 (number-sequence 0 (1- (length vars))))))) 155 156 (defun org-babel-julia-quote-csv-field (s) 157 "Quote field S for export to julia." 158 (if (stringp s) 159 (concat "\"" (mapconcat #'identity (split-string s "\"") "\"\"") "\"") 160 (format "%S" s))) 161 162 (defun org-babel-julia-assign-elisp (name value) 163 "Construct julia code assigning the elisp VALUE to a variable named NAME." 164 (if (listp value) 165 (let* ((lengths (mapcar #'length (cl-remove-if-not #'sequencep value))) 166 (max (if lengths (apply #'max lengths) 0)) 167 (min (if lengths (apply #'min lengths) 0))) 168 ;; Ensure VALUE has an orgtbl structure (depth of at least 2). 169 (unless (listp (car value)) (setq value (list value))) 170 (let ((file (orgtbl-to-csv value '(:fmt org-babel-julia-quote-csv-field)))) 171 (if (= max min) 172 (format "%s = begin 173 using CSV 174 CSV.read(\"%s\") 175 end" name file) 176 (format "%s = begin 177 using CSV 178 CSV.read(\"%s\") 179 end" 180 name file)))) 181 (format "%s = %s" name (org-babel-julia-quote-csv-field value)))) 182 183 (defvar ess-ask-for-ess-directory) ; dynamically scoped 184 (defun org-babel-julia-initiate-session (session params) 185 "If there is not a current julia process then create one." 186 (unless (string= session "none") 187 (let* ((session (or session "*Julia*")) 188 (ess-ask-for-ess-directory 189 (and (bound-and-true-p ess-ask-for-ess-directory) 190 (not (cdr (assq :dir params))))) 191 ;; Make ESS name the process buffer as SESSION. 192 (ess-gen-proc-buffer-name-function 193 (lambda (_) session))) 194 (if (org-babel-comint-buffer-livep session) 195 session 196 ;; FIXME: Depending on `display-buffer-alist', (julia) may end up 197 ;; popping up a new frame which `save-window-excursion' won't be able 198 ;; to "undo", so we really should call a kind of 199 ;; `julia-no-select' instead so we don't need to undo any 200 ;; window-changes afterwards. 201 (save-window-excursion 202 (when (get-buffer session) 203 ;; Session buffer exists, but with dead process 204 (set-buffer session)) 205 (org-require-package 'ess "ESS") 206 (set-buffer (julia)) 207 (current-buffer)))))) 208 209 (defun org-babel-julia-graphical-output-file (params) 210 "Name of file to which julia should send graphical output." 211 (and (member "graphics" (cdr (assq :result-params params))) 212 (cdr (assq :file params)))) 213 214 (defconst org-babel-julia-eoe-indicator "print(\"org_babel_julia_eoe\")") 215 (defconst org-babel-julia-eoe-output "org_babel_julia_eoe") 216 217 (defconst org-babel-julia-write-object-command "begin 218 local p_ans = %s 219 local p_tmp_file = \"%s\" 220 221 try 222 using CSV, DataFrames 223 224 if typeof(p_ans) <: DataFrame 225 p_ans_df = p_ans 226 else 227 p_ans_df = DataFrame(:ans => p_ans) 228 end 229 230 CSV.write(p_tmp_file, 231 p_ans_df, 232 writeheader = %s, 233 transform = (col, val) -> something(val, missing), 234 missingstring = \"nil\", 235 quotestrings = false) 236 p_ans 237 catch e 238 err_msg = \"Source block evaluation failed. $e\" 239 CSV.write(p_tmp_file, 240 DataFrame(:ans => err_msg), 241 writeheader = false, 242 transform = (col, val) -> something(val, missing), 243 missingstring = \"nil\", 244 quotestrings = false) 245 246 err_msg 247 end 248 end") 249 250 (defun org-babel-julia-evaluate 251 (session body result-type result-params column-names-p) 252 "Evaluate julia code in BODY." 253 (if session 254 (org-babel-julia-evaluate-session 255 session body result-type result-params column-names-p) 256 (org-babel-julia-evaluate-external-process 257 body result-type result-params column-names-p))) 258 259 (defun org-babel-julia-evaluate-external-process 260 (body result-type result-params column-names-p) 261 "Evaluate BODY in external julia process. 262 If RESULT-TYPE equals `output' then return standard output as a 263 string. If RESULT-TYPE equals `value' then return the value of the 264 last statement in BODY, as elisp." 265 (cl-case result-type 266 (value 267 (let ((tmp-file (org-babel-temp-file "julia-"))) 268 (org-babel-eval org-babel-julia-command 269 (format org-babel-julia-write-object-command 270 (format "begin %s end" body) 271 (org-babel-process-file-name tmp-file 'noquote) 272 (if column-names-p "true" "false") 273 )) 274 (org-babel-julia-process-value-result 275 (org-babel-result-cond result-params 276 (with-temp-buffer 277 (insert-file-contents tmp-file) 278 (buffer-string)) 279 (org-babel-import-elisp-from-file tmp-file '(4))) 280 column-names-p))) 281 (output (org-babel-eval org-babel-julia-command body)))) 282 283 (defun org-babel-julia-evaluate-session 284 (session body result-type result-params column-names-p) 285 "Evaluate BODY in SESSION. 286 If RESULT-TYPE equals `output' then return standard output as a 287 string. If RESULT-TYPE equals `value' then return the value of the 288 last statement in BODY, as elisp." 289 (cl-case result-type 290 (value 291 (with-temp-buffer 292 (insert (org-babel-chomp body)) 293 (let ((ess-local-customize-alist t) 294 (ess-local-process-name 295 (process-name (get-buffer-process session))) 296 (ess-eval-visibly-p nil)) 297 (ess-eval-buffer nil))) 298 (let ((tmp-file (org-babel-temp-file "julia-"))) 299 (org-babel-comint-eval-invisibly-and-wait-for-file 300 session tmp-file 301 (format org-babel-julia-write-object-command 302 "ans" 303 (org-babel-process-file-name tmp-file 'noquote) 304 (if column-names-p "true" "false") 305 )) 306 (org-babel-julia-process-value-result 307 (org-babel-result-cond result-params 308 (with-temp-buffer 309 (insert-file-contents tmp-file) 310 (buffer-string)) 311 (org-babel-import-elisp-from-file tmp-file '(4))) 312 column-names-p))) 313 (output 314 (mapconcat 315 #'org-babel-chomp 316 (butlast 317 (delq nil 318 (mapcar 319 (lambda (line) (when (> (length line) 0) line)) 320 (mapcar 321 (lambda (line) ;; cleanup extra prompts left in output 322 (if (string-match 323 "^\\([>+.]\\([ ][>.+]\\)*[ ]\\)" 324 (car (split-string line "\n"))) 325 (substring line (match-end 1)) 326 line)) 327 (org-babel-comint-with-output (session org-babel-julia-eoe-output) 328 (insert (mapconcat #'org-babel-chomp 329 (list body org-babel-julia-eoe-indicator) 330 "\n")) 331 (inferior-ess-send-input)))))) 332 "\n")))) 333 334 (defun org-babel-julia-process-value-result (result column-names-p) 335 "Julia-specific processing of return value. 336 Insert hline if column names in output have been requested." 337 (if column-names-p 338 (cons (car result) (cons 'hline (cdr result))) 339 result)) 340 341 (provide 'ob-julia) 342 343 ;;; ob-julia.el ends here