Babel Lisp header values and the user's shell !154

merged merged by cmc on 2026-10-08 01:54 UTC · krz/orgstar:babel-lisp-headers into main

3 files changed, +65 −17

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +55 −15
@@ -113,8 +113,9 @@ public enum Babel {
113113
114114 public struct Options: Sendable, Equatable {
115115 public var python = "python3"
116 /// `shell-file-name`, for blocks in the `shell` language.
117 public var shell = "/bin/sh"
116 /// `shell-file-name`, from `SHELL` as Emacs sets it: runs blocks in the `shell` language
117 /// and chooses how variables are written in the `:cache` hash.
118 public var shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/sh"
118119 public init() {}
119120 }
120121
@@ -262,6 +263,8 @@ public enum Babel {
262263 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
263264 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> {
264265 let refs = refs ?? References(resolved: [:], options: options)
266 var params = params
267 params.evaluateLisp()
265268 guard let language = block.language else { return .failure(.message("This block has no language")) }
266269 let eval = params.single[":eval"]?.lowercased()
267270 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
@@ -298,7 +301,7 @@ public enum Babel {
298301 let finishBase = { (kind: BabelJob.Finish.Kind) in
299302 BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline,
300303 file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash,
301 colnames: tables.picked(tables.colnames, params.single[":colnames"]), rownames: tables.picked(tables.rownames, params.single[":rownames"]))
304 colnames: tables.picked(tables.colnames, ":colnames", params), rownames: tables.picked(tables.rownames, ":rownames", params))
302305 }
303306 let vars: [(String, VarValue)]
304307 var stdin: VarValue?
@@ -581,14 +584,26 @@ public enum Babel {
581584 var colnames: [(name: String, names: [Cell])] = []
582585 var rownames: [(name: String, names: [Cell])] = []
583586
584 /// `org-babel-pick-name`: the last variable's names, unless the header argument is
585 /// absent, whose `nil` selects none.
586 func picked(_ names: [(name: String, names: [Cell])], _ selector: String?) -> [String]? {
587 guard selector != nil, let last = names.last else { return nil }
587 /// `org-babel-pick-name`: a Lisp list given as the header argument, or the last
588 /// variable's names, unless the header argument is absent, whose `nil` selects none.
589 func picked(_ names: [(name: String, names: [Cell])], _ key: String, _ params: Params) -> [String]? {
590 if let list = params.evaluated[key].flatMap(Babel.lispNames) { return list }
591 guard params.single[key] != nil, let last = names.last else { return nil }
588592 return last.names.map(Babel.cellText)
589593 }
590594 }
591595
596 /// A Lisp list of strings and numbers, as names.
597 static func lispNames(_ value: Sexp) -> [String]? {
598 guard case .list(let items) = value else { return nil }
599 return try? items.map { item in
600 if let s = item.string { return s }
601 let printed = Elisp.printed(item)
602 guard number(printed) == printed else { throw Failure.message(printed) }
603 return printed
604 }
605 }
606
592607 static func cellText(_ c: Cell) -> String {
593608 switch c {
594609 case .number(let n): n
@@ -600,8 +615,8 @@ public enum Babel {
600615 /// by default when only its second row is a rule; its first column with `:rownames`; its
601616 /// rules unless `:hlines yes`.
602617 static func disassemble(_ vars: [(String, VarValue)], params: Params) throws -> Tables {
603 for key in [":colnames", ":rownames"] where params.lisp.contains(key) {
604 throw Failure.message("\(key) with a Lisp value isn't supported yet; nothing was run.")
618 for key in [":colnames", ":rownames"] where params.lisp.contains(key) && params.evaluated[key].flatMap(lispNames) == nil {
619 throw Failure.message("\(key) \(params.single[key] ?? "") is Lisp that only Emacs can evaluate; nothing was run.")
605620 }
606621 let colnames = params.single[":colnames"]
607622 let rownames = params.single[":rownames"]
@@ -708,8 +723,8 @@ public enum Babel {
708723 /// What `org-babel-sha1-hash` hashes: the header arguments after `org-babel-process-params`,
709724 /// sorted by key and printed with `%S`, then the expanded body.
710725 static func cacheInput(language: String, params: Params, tables: Tables, expanded: String, call: Bool) throws -> String {
711 if !params.lisp.isEmpty {
712 throw Failure.message(":cache with Lisp header values isn't supported yet; nothing was run.")
726 if let key = params.lisp.first(where: { params.evaluated[$0] == nil }) {
727 throw Failure.message(":cache with \(key) \(params.single[key] ?? "") needs Emacs to evaluate the Lisp; nothing was run.")
713728 }
714729 let ignored: Set<String> = ["replace", "silent", "none", "discard", "append", "prepend"]
715730 let drawsFile = language == "dot" || language == "plantuml"
@@ -719,7 +734,9 @@ public enum Babel {
719734 var entries: [(key: String, printed: String)] = []
720735 for (key, value) in defaults.merging(params.single, uniquingKeysWith: { $1 }) {
721736 let printed: String
722 if value.isEmpty {
737 if let evaluated = params.evaluated[key] {
738 printed = Elisp.printed(evaluated, escape: true)
739 } else if value.isEmpty {
723740 continue
724741 } else if key == ":exports" {
725742 printed = lispString(value.split(whereSeparator: \.isWhitespace).map(String.init).filter { !ignored.contains($0) }.sorted().joined(separator: " "))
@@ -803,10 +820,32 @@ public enum Babel {
803820 var single: [String: String] = [:]
804821 var vars: [(name: String, value: String)] = []
805822 var wrap: String?
806 /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`.
823 /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`, `'`, `` ` `` or `[`.
807824 var lisp: Set<String> = []
808825 /// Keys whose value was a quoted string, which `org-babel-read` doesn't take for a number.
809826 var quoted: Set<String> = []
827 /// Lisp values evaluated to something other than a string or number, such as a list of
828 /// column names.
829 var evaluated: [String: Sexp] = [:]
830
831 /// `org-babel-read` on the Lisp values the interpreter can evaluate: a string or number
832 /// takes the form's place; other values are kept in `evaluated`.
833 mutating func evaluateLisp() {
834 for key in lisp {
835 guard let form = try? LispReader.readFirst(single[key] ?? "").sexp, let value = try? Elisp().eval(form) else { continue }
836 let printed = Elisp.printed(value)
837 if let s = value.string {
838 single[key] = s
839 quoted.insert(key)
840 lisp.remove(key)
841 } else if Babel.number(printed) == printed {
842 single[key] = printed
843 lisp.remove(key)
844 } else {
845 evaluated[key] = value
846 }
847 }
848 }
810849
811850 static let resultGroups: [Set<String>] = [
812851 ["file", "list", "vector", "table", "scalar", "verbatim"],
@@ -859,7 +898,7 @@ public enum Babel {
859898 let quoted = key != ":var" && raw.count >= 2 && raw.hasPrefix("\"") && raw.hasSuffix("\"")
860899 let value = quoted ? Params.unescape(String(raw.dropFirst().dropLast())) : raw
861900 if key != ":var" {
862 if !quoted, value.hasPrefix("(") { lisp.insert(key) } else { lisp.remove(key) }
901 if !quoted, let first = value.first, "('`[".contains(first) { lisp.insert(key) } else { lisp.remove(key) }
863902 if quoted { self.quoted.insert(key) } else { self.quoted.remove(key) }
864903 }
865904 switch key {
@@ -1060,7 +1099,8 @@ public enum Babel {
10601099 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? []
10611100 var inner = refs
10621101 inner.visiting.insert(name)
1063 let params = try Params(block: block, model: model, text: text, extra: extra)
1102 var params = try Params(block: block, model: model, text: text, extra: extra)
1103 params.evaluateLisp()
10641104 // A cached block gives its result without running, as `org-babel-execute-src-block` does.
10651105 var useCache = true
10661106 if params.single[":cache"] == "yes", let language = block.language {
Tests/OrgCoreTests/BabelCacheTests.swift +6 −2
@@ -33,6 +33,10 @@ struct BabelCacheTests {
3333 runs: ["#+begin_src", "#+begin_src"]),
3434 Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :cache yes :var t=kv :separator ,\necho $t\n#+end_src\n",
3535 runs: ["#+begin_src", "#+begin_src"]),
36 Case(text: "#+begin_src sh :cache yes :dir (concat \"/\" \"usr\") :foo (list 1 \"a\") :bar (+ 1 2)\npwd\n#+end_src\n",
37 runs: ["#+begin_src", "#+begin_src"]),
38 Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
39 runs: ["#+begin_src", "#+begin_src"]),
3640 // Edited after the first run, so it runs again.
3741 Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]),
3842 // A reference to a cached block takes its result without running it.
@@ -93,12 +97,12 @@ struct BabelCacheTests {
9397 }
9498
9599 @Test func lispHeadersAreRefused() {
96 let text = "#+begin_src sh :cache yes :dir (concat \"/\" \"tmp\")\necho hi\n#+end_src\n"
100 let text = "#+begin_src sh :cache yes :dir (expand-file-name \"x\")\necho hi\n#+end_src\n"
97101 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
98102 guard case .failure(.message(let message)) = Babel.plan(context) else {
99103 Issue.record("planned")
100104 return
101105 }
102 #expect(message.hasPrefix(":cache with Lisp header values"))
106 #expect(message.hasPrefix(":cache with :dir (expand-file-name"))
103107 }
104108}
Tests/OrgCoreTests/BabelTests.swift +4
@@ -189,6 +189,10 @@ struct BabelTests {
189189 "#+begin_src bash :var l='(1 \"two\" 3) :results output\necho ${l[2]} ${l[1]}\n#+end_src\n",
190190 "#+NAME: items\n- one\n- two\n\n#+begin_src zsh :var l=items :results output\necho \"$l\"\n#+end_src\n",
191191 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
192 "TBL#+begin_src python :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
193 "TBL#+begin_src python :var t=tbl :rownames '(\"r1\" \"r2\")\nreturn t\n#+end_src\n",
194 "#+begin_src sh :dir (concat \"/\" \"usr\") :results output\npwd\n#+end_src\n",
195 "#+NAME: items\n- one\n- two\n\n#+begin_src shell :var l=items :results output\necho \"$l\"\n#+end_src\n",
192196 // Lisp values.
193197 "#+begin_src python :var l='(1 2) m='((1 2) (3 4)) n=(+ 1 2) s='(\"a\" \"b\")\nreturn [l, m, n, s]\n#+end_src\n",
194198 "#+begin_src emacs-lisp :var l='(1 2) h='((1 2) hline (3 4)) s=(concat \"a\" \"b\")\n(list l h s)\n#+end_src\n",