krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 3e80720dbf

3e80720dbff31d31d0bb2f4b66335e8b8b81f78e

parent: 3ed8024d96

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 02:05 UTC

Babel file Lisp, PWD, and buffers by truename

Lisp header values that need the file (buffer-file-name, expand-file-name)
are evaluated by planning again with the path before the run, so :cache
hashes them as Emacs does. Blocks run with PWD set, as call-process does.
Opening a file by another path shows its open buffer, and buffers of the
same name are named as uniquify does.

Layout: unified · split

Sources/OrgApp/BabelRunner.swift +2
@@ -53,6 +53,8 @@ public enum BabelRunner {
53 let directory = job.directory.map { URL(fileURLWithPath: $0, relativeTo: fileDirectory).standardizedFileURL } ?? fileDirectory 53 let directory = job.directory.map { URL(fileURLWithPath: $0, relativeTo: fileDirectory).standardizedFileURL } ?? fileDirectory
54 guard FileManager.default.fileExists(atPath: directory.path) else { throw Failure(description: "No folder \(directory.path) for :dir.") } 54 guard FileManager.default.fileExists(atPath: directory.path) else { throw Failure(description: "No folder \(directory.path) for :dir.") }
55 process.currentDirectoryURL = directory 55 process.currentDirectoryURL = directory
56 // As Emacs's `call-process` does, so `pwd` prints the folder as written, not resolved.
57 process.environment?["PWD"] = directory.path
56 let stdout = folder.appendingPathComponent("stdout") 58 let stdout = folder.appendingPathComponent("stdout")
57 FileManager.default.createFile(atPath: stdout.path, contents: nil) 59 FileManager.default.createFile(atPath: stdout.path, contents: nil)
58 let out = try FileHandle(forWritingTo: stdout) 60 let out = try FileHandle(forWritingTo: stdout)
Sources/OrgApp/DocumentSession.swift +27 −1
@@ -147,7 +147,26 @@ public final class DocumentSession {
147 public var caret: Int { buffer?.selection.first?.lowerBound ?? 0 } 147 public var caret: Int { buffer?.selection.first?.lowerBound ?? 0 }
148 private var name: String { path.map { ($0 as NSString).lastPathComponent } ?? "" } 148 private var name: String { path.map { ($0 as NSString).lastPathComponent } ?? "" }
149 149
150 private func entry(_ path: String) -> Entry? { entries.first { $0.path == path } } 150 /// The open buffer of `path`, or of the same file by another path (`find-buffer-visiting`
151 /// by truename).
152 private func entry(_ path: String) -> Entry? {
153 if let exact = entries.first(where: { $0.path == path }) { return exact }
154 let truename = URL(fileURLWithPath: path).resolvingSymlinksInPath().path
155 return entries.first { URL(fileURLWithPath: $0.path).resolvingSymlinksInPath().path == truename }
156 }
157
158 /// The buffer's name as uniquify writes it (`post-forward-angle-brackets`): the file name,
159 /// with as many of its folders as tell it apart from other open files of the same name.
160 public func bufferName(_ path: String) -> String {
161 let name = (path as NSString).lastPathComponent
162 let others = openPaths.filter { $0 != path && ($0 as NSString).lastPathComponent == name }
163 guard !others.isEmpty else { return name }
164 func folders(_ p: String) -> [String] { (p as NSString).deletingLastPathComponent.split(separator: "/").map(String.init).reversed() }
165 let mine = folders(path)
166 var count = 1
167 while count < mine.count, others.contains(where: { folders($0).prefix(count) == mine.prefix(count) }) { count += 1 }
168 return name + "<" + mine.prefix(count).reversed().joined(separator: "/") + ">"
169 }
151 170
152 public func isOpen(_ path: String) -> Bool { entry(path) != nil } 171 public func isOpen(_ path: String) -> Bool { entry(path) != nil }
153 172
@@ -727,6 +746,13 @@ public final class DocumentSession {
727 var job = job 746 var job = job
728 let approved = job.approved 747 let approved = job.approved
729 func agreed(_ block: BabelJob) -> Bool { approved.isEmpty || approved.contains(block.blockText) } 748 func agreed(_ block: BabelJob) -> Bool { approved.isEmpty || approved.contains(block.blockText) }
749 if job.needsFile {
750 let document = entry.buffer.document
751 switch Babel.replan(job, file: entry.path, text: document.text, tree: document.tree) {
752 case .failure(.message(let message)): return message
753 case .success(let next): job = next
754 }
755 }
730 while let need = job.needs.first { 756 while let need = job.needs.first {
731 guard agreed(need.job) else { return "The blocks changed while running; nothing more was run." } 757 guard agreed(need.job) else { return "The blocks changed while running; nothing more was run." }
732 let output = try await run(need.job) 758 let output = try await run(need.job)
Sources/OrgCore/Compute/Babel.swift +33 −5
@@ -44,6 +44,9 @@ public struct BabelJob: Sendable, Equatable {
44 public internal(set) var script: String? 44 public internal(set) var script: String?
45 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`. 45 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`.
46 public internal(set) var lispForm: String? 46 public internal(set) var lispForm: String?
47 /// A Lisp header value couldn't be evaluated without the file's path: plan again with
48 /// `Babel.replan` before running.
49 public internal(set) var needsFile = false
47 50
48 public struct Need: Sendable, Equatable { 51 public struct Need: Sendable, Equatable {
49 /// The `:var` value, `name` or `name(args)`. 52 /// The `:var` value, `name` or `name(args)`.
@@ -116,6 +119,8 @@ public enum Babel {
116 /// `shell-file-name`, from `SHELL` as Emacs sets it: runs blocks in the `shell` language 119 /// `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. 120 /// and chooses how variables are written in the `:cache` hash.
118 public var shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/sh" 121 public var shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/sh"
122 /// The file the block is in, for Lisp header values that use it.
123 public var file: String?
119 public init() {} 124 public init() {}
120 } 125 }
121 126
@@ -135,6 +140,14 @@ public enum Babel {
135 return plan(context, options: job.options, resolved: resolved) 140 return plan(context, options: job.options, resolved: resolved)
136 } 141 }
137 142
143 /// Plans `job`'s block again knowing the file it is in.
144 public static func replan(_ job: BabelJob, file: String, text: String, tree: OrgTree) -> Result<BabelJob, Failure> {
145 var options = job.options
146 options.file = file
147 let context = EditContext(revision: 0, text: text, tree: tree, selection: [job.caret..<job.caret])
148 return plan(context, options: options, resolved: job.resolved)
149 }
150
138 static func plan(_ context: EditContext, options: Options, resolved: [String: VarValue]) -> Result<BabelJob, Failure> { 151 static func plan(_ context: EditContext, options: Options, resolved: [String: VarValue]) -> Result<BabelJob, Failure> {
139 let text = context.text 152 let text = context.text
140 let ns = text as NSString 153 let ns = text as NSString
@@ -262,9 +275,23 @@ public enum Babel {
262 275
263 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options, 276 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
264 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> { 277 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> {
278 var evaluated = params
279 evaluated.evaluateLisp(file: options.file)
280 let needsFile = options.file == nil && evaluated.lisp.contains { evaluated.evaluated[$0] == nil }
281 return planBlock(block, params: params, model: model, text: ns, options: options, location: location, name: name, inline: inline,
282 refs: refs, useCache: useCache).map { job in
283 var job = job
284 job.needsFile = needsFile
285 return job
286 }
287 }
288
289 static func planBlock(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
290 location: Range<Int>, name: String?, inline: Bool, refs: References?, useCache: Bool) -> Result<BabelJob, Failure> {
265 let refs = refs ?? References(resolved: [:], options: options) 291 let refs = refs ?? References(resolved: [:], options: options)
266 var params = params 292 var params = params
267 params.evaluateLisp() 293 params.evaluateLisp(file: options.file)
294 let needsFile = options.file == nil && params.lisp.contains { params.evaluated[$0] == nil }
268 guard let language = block.language else { return .failure(.message("This block has no language")) } 295 guard let language = block.language else { return .failure(.message("This block has no language")) }
269 let eval = params.single[":eval"]?.lowercased() 296 let eval = params.single[":eval"]?.lowercased()
270 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { 297 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
@@ -326,7 +353,7 @@ public enum Babel {
326 } catch { 353 } catch {
327 return .failure(.message("\(error)")) 354 return .failure(.message("\(error)"))
328 } 355 }
329 if useCache, !inline, params.single[":cache"] == "yes" { 356 if useCache, !inline, !needsFile, params.single[":cache"] == "yes" {
330 do { 357 do {
331 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name, 358 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name,
332 call: location != block.range, text: ns, options: options) 359 call: location != block.range, text: ns, options: options)
@@ -830,9 +857,10 @@ public enum Babel {
830 857
831 /// `org-babel-read` on the Lisp values the interpreter can evaluate: a string or number 858 /// `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`. 859 /// takes the form's place; other values are kept in `evaluated`.
833 mutating func evaluateLisp() { 860 mutating func evaluateLisp(file: String?) {
834 for key in lisp { 861 for key in lisp {
835 guard let form = try? LispReader.readFirst(single[key] ?? "").sexp, let value = try? Elisp().eval(form) else { continue } 862 guard let form = try? LispReader.readFirst(single[key] ?? "").sexp,
863 let value = try? (file.map { Tangle.fileLisp(path: $0) } ?? Elisp()).eval(form) else { continue }
836 let printed = Elisp.printed(value) 864 let printed = Elisp.printed(value)
837 if let s = value.string { 865 if let s = value.string {
838 single[key] = s 866 single[key] = s
@@ -1100,7 +1128,7 @@ public enum Babel {
1100 var inner = refs 1128 var inner = refs
1101 inner.visiting.insert(name) 1129 inner.visiting.insert(name)
1102 var params = try Params(block: block, model: model, text: text, extra: extra) 1130 var params = try Params(block: block, model: model, text: text, extra: extra)
1103 params.evaluateLisp() 1131 params.evaluateLisp(file: refs.options.file)
1104 // A cached block gives its result without running, as `org-babel-execute-src-block` does. 1132 // A cached block gives its result without running, as `org-babel-execute-src-block` does.
1105 var useCache = true 1133 var useCache = true
1106 if params.single[":cache"] == "yes", let language = block.language { 1134 if params.single[":cache"] == "yes", let language = block.language {
Sources/OrgCore/Compute/Tangle.swift +10 −4
@@ -135,10 +135,9 @@ public enum Tangle {
135 return params 135 return params
136 } 136 }
137 137
138 /// `org-babel-read` of a header value written as Lisp, with what a file's buffer knows: 138 /// An interpreter with what a file's buffer knows: `buffer-file-name`, `default-directory`,
139 /// `buffer-file-name`, `default-directory` and `system-type`. 139 /// `system-type` and the file name functions.
140 static func evaluate(_ value: String, key: String, path: String) throws -> String { 140 static func fileLisp(path: String) -> Elisp {
141 let refusal = Babel.Failure.message("\(key) \(value) is Lisp tangling can't evaluate yet; nothing was tangled.")
142 let lisp = Elisp() 141 let lisp = Elisp()
143 let directory = (path as NSString).deletingLastPathComponent + "/" 142 let directory = (path as NSString).deletingLastPathComponent + "/"
144 lisp.set("buffer-file-name", .string(path)) 143 lisp.set("buffer-file-name", .string(path))
@@ -170,6 +169,13 @@ public enum Tangle {
170 let name = try string(args, 0) 169 let name = try string(args, 0)
171 return .string(name.hasSuffix("/") ? name : name + "/") 170 return .string(name.hasSuffix("/") ? name : name + "/")
172 } 171 }
172 return lisp
173 }
174
175 /// `org-babel-read` of a header value written as Lisp, in `fileLisp`.
176 static func evaluate(_ value: String, key: String, path: String) throws -> String {
177 let refusal = Babel.Failure.message("\(key) \(value) is Lisp tangling can't evaluate yet; nothing was tangled.")
178 let lisp = fileLisp(path: path)
173 guard let form = try? LispReader.readFirst(value).sexp, let result = try? lisp.eval(form), !Elisp.isNil(result) else { throw refusal } 179 guard let form = try? LispReader.readFirst(value).sexp, let result = try? lisp.eval(form), !Elisp.isNil(result) else { throw refusal }
174 switch result { 180 switch result {
175 case .string(let s): return s 181 case .string(let s): return s
Sources/Orgstar/ContentView.swift +4 −9
@@ -204,7 +204,7 @@ struct ContentView: View {
204 } 204 }
205 } 205 }
206 } 206 }
207 .navigationTitle((previewPath ?? session.path).map { ($0 as NSString).lastPathComponent } ?? "Orgstar") 207 .navigationTitle(previewPath.map { ($0 as NSString).lastPathComponent } ?? session.path.map(session.bufferName) ?? "Orgstar")
208 .navigationSubtitle(session.isReadOnly ? "Read-only: not UTF-8" : "") 208 .navigationSubtitle(session.isReadOnly ? "Read-only: not UTF-8" : "")
209 .searchable(text: $query, placement: .toolbar, prompt: "Search notes") 209 .searchable(text: $query, placement: .toolbar, prompt: "Search notes")
210 .searchFocused($searchFocused) 210 .searchFocused($searchFocused)
@@ -511,12 +511,7 @@ struct ContentView: View {
511 /// `SPC b b`: open buffers, most recent first, by name (with the folder when names repeat). 511 /// `SPC b b`: open buffers, most recent first, by name (with the folder when names repeat).
512 private func switchBuffer() { 512 private func switchBuffer() {
513 let paths = session.recentPaths.dropFirst() + session.recentPaths.prefix(1) 513 let paths = session.recentPaths.dropFirst() + session.recentPaths.prefix(1)
514 let names = paths.map { ($0 as NSString).lastPathComponent } 514 let labels = paths.map(session.bufferName)
515 let labels = paths.map { path in
516 let name = (path as NSString).lastPathComponent
517 guard names.filter({ $0 == name }).count > 1 else { return name }
518 return name + " — " + ((path as NSString).deletingLastPathComponent as NSString).lastPathComponent
519 }
520 guard !labels.isEmpty else { return commands.show("No buffers are open") } 515 guard !labels.isEmpty else { return commands.show("No buffers are open") }
521 commands.ask(Prompt(key: "buffer", message: "Switch to buffer:", choices: labels)) { answer in 516 commands.ask(Prompt(key: "buffer", message: "Switch to buffer:", choices: labels)) { answer in
522 guard let answer, let index = labels.firstIndex(of: answer) else { return } 517 guard let answer, let index = labels.firstIndex(of: answer) else { return }
@@ -896,7 +891,7 @@ struct BufferTabs: View {
896 private func tab(_ path: String) -> some View { 891 private func tab(_ path: String) -> some View {
897 let current = path == session.path 892 let current = path == session.path
898 return HStack(spacing: 6) { 893 return HStack(spacing: 6) {
899 Text((path as NSString).lastPathComponent) 894 Text(session.bufferName(path))
900 .lineLimit(1) 895 .lineLimit(1)
901 .foregroundStyle(current ? .primary : .secondary) 896 .foregroundStyle(current ? .primary : .secondary)
902 .accessibilityAddTraits(current ? [.isButton, .isSelected] : .isButton) 897 .accessibilityAddTraits(current ? [.isButton, .isSelected] : .isButton)
@@ -914,7 +909,7 @@ struct BufferTabs: View {
914 } 909 }
915 .buttonStyle(.borderless) 910 .buttonStyle(.borderless)
916 .help("Close") 911 .help("Close")
917 .accessibilityLabel("Close \((path as NSString).lastPathComponent)" + (session.dirtyPaths.contains(path) ? ", unsaved" : "")) 912 .accessibilityLabel("Close \(session.bufferName(path))" + (session.dirtyPaths.contains(path) ? ", unsaved" : ""))
918 } 913 }
919 .font(.callout) 914 .font(.callout)
920 .padding(.horizontal, 12) 915 .padding(.horizontal, 12)
Sources/OrgstarMobile/EditorScreen.swift +1 −1
@@ -55,7 +55,7 @@ struct EditorScreen: View {
55 ProgressView() 55 ProgressView()
56 } 56 }
57 } 57 }
58 .navigationTitle((path as NSString).lastPathComponent + (session.dirtyPaths.contains(path) ? " •" : "")) 58 .navigationTitle(session.bufferName(path) + (session.dirtyPaths.contains(path) ? " •" : ""))
59 .navigationBarTitleDisplayMode(.inline) 59 .navigationBarTitleDisplayMode(.inline)
60 .toolbar(.hidden, for: .tabBar) 60 .toolbar(.hidden, for: .tabBar)
61 .toolbar { 61 .toolbar {
Tests/OrgAppTests/AppTests.swift +27
@@ -190,6 +190,33 @@ struct DocumentSessionTests {
190 #expect(session.revealRequest?.offset == 12) 190 #expect(session.revealRequest?.offset == 12)
191 } 191 }
192 192
193 @Test func sameFileByAnotherPathSharesItsBuffer() throws {
194 let notes = try Folder()
195 let file = try notes.write("a.org", "* One\n")
196 let link = notes.url.appendingPathComponent("link")
197 try FileManager.default.createSymbolicLink(at: link, withDestinationURL: notes.url)
198 let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r")))
199 session.open(file.path)
200 session.open(link.appendingPathComponent("a.org").path)
201 #expect(session.openPaths == [file.path])
202 #expect(session.path == file.path)
203 }
204
205 @Test func buffersOfTheSameNameShowTheirFolders() throws {
206 let notes = try Folder()
207 let a = try notes.write("x/blog/index.org", "a\n")
208 let b = try notes.write("y/blog/index.org", "b\n")
209 let c = try notes.write("y/other.org", "c\n")
210 let session = DocumentSession(recovery: FileRecoveryStore(directory: notes.url.appendingPathComponent(".r")))
211 session.open(a.path)
212 #expect(session.bufferName(a.path) == "index.org")
213 session.open(b.path)
214 session.open(c.path)
215 #expect(session.bufferName(a.path) == "index.org<x/blog>")
216 #expect(session.bufferName(b.path) == "index.org<y/blog>")
217 #expect(session.bufferName(c.path) == "other.org")
218 }
219
193 @Test func openReadsSetupFiles() throws { 220 @Test func openReadsSetupFiles() throws {
194 let notes = try Folder() 221 let notes = try Folder()
195 try notes.write("setup.org", "#+TODO: NEXT | DONE\n") 222 try notes.write("setup.org", "#+TODO: NEXT | DONE\n")
Tests/OrgCoreTests/BabelCacheTests.swift +30 −3
@@ -96,13 +96,40 @@ struct BabelCacheTests {
96 #expect(second.failure == "Cached: \"hi\"") 96 #expect(second.failure == "Cached: \"hi\"")
97 } 97 }
98 98
99 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
100 func fileLispMatchesEmacs() throws {
101 let text = "#+begin_src sh :cache yes :dir (file-name-directory buffer-file-name) :foo (file-name-nondirectory buffer-file-name)\npwd\n#+end_src\n"
102 let emacs = try EmacsOracle.evaluate(text, """
103 (progn (require 'ob-shell) (goto-char (point-min))
104 (let ((info (org-babel-get-src-block-info)))
105 (setf (nth 2 info) (org-babel-process-params (nth 2 info)))
106 (list buffer-file-name (org-babel-sha1-hash info :eval))))
107 """)
108 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
109 guard case .success(let first) = Babel.plan(context) else {
110 Issue.record("not planned")
111 return
112 }
113 #expect(first.needsFile)
114 #expect(first.finish.hash == nil)
115 guard case .success(let job) = Babel.replan(first, file: emacs[0], text: text, tree: context.tree) else {
116 Issue.record("not replanned")
117 return
118 }
119 #expect(!job.needsFile)
120 #expect(job.finish.hash == emacs[1])
121 #expect(job.directory == (emacs[0] as NSString).deletingLastPathComponent + "/")
122 }
123
99 @Test func lispHeadersAreRefused() { 124 @Test func lispHeadersAreRefused() {
100 let text = "#+begin_src sh :cache yes :dir (expand-file-name \"x\")\necho hi\n#+end_src\n" 125 let text = "#+begin_src sh :cache yes :dir (user-full-name)\necho hi\n#+end_src\n"
101 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2]) 126 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
102 guard case .failure(.message(let message)) = Babel.plan(context) else { 127 var options = Babel.Options()
128 options.file = "/tmp/a.org"
129 guard case .failure(.message(let message)) = Babel.plan(context, options: options) else {
103 Issue.record("planned") 130 Issue.record("planned")
104 return 131 return
105 } 132 }
106 #expect(message.hasPrefix(":cache with :dir (expand-file-name")) 133 #expect(message.hasPrefix(":cache with :dir (user-full-name)"))
107 } 134 }
108} 135}
Tests/OrgCoreTests/BabelTests.swift +3 −1
@@ -30,7 +30,8 @@ enum TestBabelRunner {
30 case .session: 30 case .session:
31 return BabelOutput(stdout: "") 31 return BabelOutput(stdout: "")
32 } 32 }
33 process.currentDirectoryURL = job.directory.map { URL(fileURLWithPath: $0, relativeTo: directory) } ?? directory 33 process.currentDirectoryURL = job.directory.map { URL(fileURLWithPath: $0, relativeTo: directory).standardizedFileURL } ?? directory
34 process.environment = ProcessInfo.processInfo.environment.merging(["PWD": process.currentDirectoryURL!.path]) { $1 }
34 let stdin = Pipe() 35 let stdin = Pipe()
35 let stdout = Pipe() 36 let stdout = Pipe()
36 process.standardInput = stdin 37 process.standardInput = stdin
@@ -192,6 +193,7 @@ struct BabelTests {
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 :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 "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 "#+begin_src sh :dir (concat \"/\" \"usr\") :results output\npwd\n#+end_src\n",
196 "#+begin_src sh :dir /tmp :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", 197 "#+NAME: items\n- one\n- two\n\n#+begin_src shell :var l=items :results output\necho \"$l\"\n#+end_src\n",
196 // Lisp values. 198 // Lisp values.
197 "#+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", 199 "#+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",