krz/orgstar

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

Commit f984b02522

f984b025224c72d67358672387608f2e128b70c9

parent: 3bb6fee240

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 01:23 UTC

Run C, C++, D, Java, Fortran and Clojure blocks

Plans a shell script that writes the expansion to a temporary file,
compiles it with gcc, g++, rdmd, javac or gfortran and runs it, or runs
it with babashka or the Clojure CLI, reading results as ob-C, ob-java,
ob-fortran and ob-clojure do. The Mac runner refuses when the program
isn't on PATH.

Java gets org-babel-default-header-args:java (:results output, :dir .),
so its :cache hash matches Emacs. D's IMPORTS and Fortran's INCLUDES and
DEFINES properties are read where the block is, for runs and the hash.

Layout: unified · split

Sources/OrgApp/BabelRunner.swift +11 −1
@@ -18,7 +18,17 @@ public enum BabelRunner {
18 return environment 18 return environment
19 } 19 }
20 20
21 /// Whether `program` is an executable file on `environment`'s PATH, or the path of one.
22 public static func installed(_ program: String) -> Bool {
23 let paths = program.contains("/") ? [program] : (environment["PATH"] ?? "").split(separator: ":").map { "\($0)/\(program)" }
24 return paths.contains { FileManager.default.isExecutableFile(atPath: $0) }
25 }
26
21 public static func run(_ job: BabelJob, fileDirectory: URL, timeout: Duration = .seconds(300)) async throws -> BabelOutput { 27 public static func run(_ job: BabelJob, fileDirectory: URL, timeout: Duration = .seconds(300)) async throws -> BabelOutput {
28 for programs in job.programs where !programs.contains(where: installed) {
29 let names = programs.count == 1 ? programs[0] : "Neither " + programs.joined(separator: " nor ")
30 throw Failure(description: "\(names) \(programs.count == 1 ? "isn't" : "is") installed, so \(job.language) blocks can't run.")
31 }
22 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-babel-\(UUID().uuidString)") 32 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-babel-\(UUID().uuidString)")
23 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) 33 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
24 defer { try? FileManager.default.removeItem(at: folder) } 34 defer { try? FileManager.default.removeItem(at: folder) }
@@ -98,7 +108,7 @@ public enum BabelRunner {
98 let output = (try? String(contentsOf: stdout, encoding: .utf8)) ?? "" 108 let output = (try? String(contentsOf: stdout, encoding: .utf8)) ?? ""
99 let errors = (try? String(contentsOf: stderr, encoding: .utf8)) ?? "" 109 let errors = (try? String(contentsOf: stderr, encoding: .utf8)) ?? ""
100 let result = try? String(contentsOf: resultFile, encoding: .utf8) 110 let result = try? String(contentsOf: resultFile, encoding: .utf8)
101 if case .process(let executable, _) = job.runner, process.terminationStatus == 127, output.isEmpty { 111 if case .process(let executable, _) = job.runner, job.programs.isEmpty, process.terminationStatus == 127, output.isEmpty {
102 throw Failure(description: "\(executable) isn't installed.") 112 throw Failure(description: "\(executable) isn't installed.")
103 } 113 }
104 return BabelOutput(stdout: output, resultFile: result, stderr: errors, status: process.terminationStatus) 114 return BabelOutput(stdout: output, resultFile: result, stderr: errors, status: process.terminationStatus)
Sources/OrgCore/Compute/Babel.swift +53 −17
@@ -42,6 +42,8 @@ public struct BabelJob: Sendable, Equatable {
42 var options = Babel.Options() 42 var options = Babel.Options()
43 /// A script for the runner to write to an executable file, for `Babel.scriptFileToken`. 43 /// A script for the runner to write to an executable file, for `Babel.scriptFileToken`.
44 public internal(set) var script: String? 44 public internal(set) var script: String?
45 /// Programs the run needs: for each list, one of them on PATH.
46 public internal(set) var programs: [[String]] = []
45 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`. 47 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`.
46 public internal(set) var lispForm: String? 48 public internal(set) var lispForm: String?
47 /// A Lisp header value couldn't be evaluated without the file's path: plan again with 49 /// A Lisp header value couldn't be evaluated without the file's path: plan again with
@@ -55,7 +57,8 @@ public struct BabelJob: Sendable, Equatable {
55 } 57 }
56 58
57 public struct Finish: Sendable, Equatable { 59 public struct Finish: Sendable, Equatable {
58 enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp } 60 /// `compiled`: C, C++, D, Java, Fortran and Clojure, by `language`.
61 enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp, compiled }
59 let kind: Kind 62 let kind: Kind
60 var language = "" 63 var language = ""
61 public internal(set) var params: [String] 64 public internal(set) var params: [String]
@@ -233,6 +236,9 @@ public enum Babel {
233 /// `org-babel-default-lob-header-args`, over the called block's own header arguments. 236 /// `org-babel-default-lob-header-args`, over the called block's own header arguments.
234 static let lobDefaults = [(":exports", "results")] 237 static let lobDefaults = [(":exports", "results")]
235 238
239 /// `org-babel-default-header-args:LANG` in Org 9.8.7 other than `:exports`.
240 static let languageDefaults = ["java": [(":results", "output"), (":dir", ".")]]
241
236 /// `:exports` from the language's `org-babel-default-header-args:LANG` in Org 9.8.7. 242 /// `:exports` from the language's `org-babel-default-header-args:LANG` in Org 9.8.7.
237 static let languageExports = ["dot": "results", "plantuml": "results", "ditaa": "results", "gnuplot": "results", 243 static let languageExports = ["dot": "results", "plantuml": "results", "ditaa": "results", "gnuplot": "results",
238 "latex": "results", "lilypond": "results"] 244 "latex": "results", "lilypond": "results"]
@@ -341,14 +347,16 @@ public enum Babel {
341 if eval == "never" || eval == "no" { 347 if eval == "never" || eval == "no" {
342 return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) 348 return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled."))
343 } 349 }
344 if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language), !shells.contains(language) { 350 let compiled = expandedLanguages.contains(language)
351 if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language), !shells.contains(language), !compiled {
345 return .failure(.message(":cmdline isn't supported for \(language) yet; nothing was run.")) 352 return .failure(.message(":cmdline isn't supported for \(language) yet; nothing was run."))
346 } 353 }
347 for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil 354 for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil
348 && !(shells.contains(language) && [":stdin", ":shebang"].contains(key)) { 355 && !(shells.contains(language) && [":stdin", ":shebang"].contains(key)) && !(compiled && [":prologue", ":epilogue"].contains(key)) {
349 return .failure(.message("\(key) isn't supported yet; nothing was run.")) 356 return .failure(.message("\(key) isn't supported yet; nothing was run."))
350 } 357 }
351 let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) } 358 // The compiled languages' `org-babel-execute:` ignore `:session`.
359 let session = compiled ? nil : params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) }
352 if session != nil, !shells.contains(language), language != "python" { 360 if session != nil, !shells.contains(language), language != "python" {
353 return .failure(.message(":session isn't supported for \(language) yet; nothing was run.")) 361 return .failure(.message(":session isn't supported for \(language) yet; nothing was run."))
354 } 362 }
@@ -400,7 +408,7 @@ public enum Babel {
400 if useCache, !inline, !needsFile, params.single[":cache"] == "yes" { 408 if useCache, !inline, !needsFile, params.single[":cache"] == "yes" {
401 do { 409 do {
402 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name, 410 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name,
403 call: location != block.range, text: ns, options: options) 411 call: location != block.range, text: ns, options: options, model: model)
404 if state.current { return .failure(.message(cachedMessage(state.value))) } 412 if state.current { return .failure(.message(cachedMessage(state.value))) }
405 // Appended or prepended results accumulate, so they carry no hash. 413 // Appended or prepended results accumulate, so they carry no hash.
406 if params.results.contains("replace") { hash = state.hash } 414 if params.results.contains("replace") { hash = state.hash }
@@ -524,6 +532,25 @@ public enum Babel {
524 input: input, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finish 532 input: input, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finish
525 )) 533 ))
526 } 534 }
535 if compiled {
536 do {
537 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= location.lowerBound }
538 let run = try compiledRun(language: language, body: body, tables: tables, params: params,
539 properties: { inheritedProperty($0, heading: heading, model: model) })
540 var finish = finishBase(.compiled)
541 // ob-clojure doesn't reassemble tables.
542 if language == "clojure" { (finish.colnames, finish.rownames) = (nil, nil) }
543 var job = BabelJob(language: language, runner: .process(executable: "/bin/sh", arguments: [scriptFileToken]), input: run.source,
544 directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finish)
545 job.script = run.script
546 job.programs = run.programs
547 return .success(job)
548 } catch let error as Failure {
549 return .failure(error)
550 } catch {
551 return .failure(.message("\(error)"))
552 }
553 }
527 if let (executable, arguments) = generic[language] { 554 if let (executable, arguments) = generic[language] {
528 guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) } 555 guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) }
529 return .success(BabelJob( 556 return .success(BabelJob(
@@ -762,10 +789,12 @@ public enum Babel {
762 /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results 789 /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results
763 /// already carry it, with their value when it can be read. 790 /// already carry it, with their value when it can be read.
764 static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?, 791 static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?,
765 call: Bool, text: NSString, options: Options) throws -> (hash: String, current: Bool, value: VarValue?) { 792 call: Bool, text: NSString, options: Options, model: DocumentModel) throws -> (hash: String, current: Bool, value: VarValue?) {
793 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= location.lowerBound }
766 // The hash is taken outside the shell's own binding of `shell-file-name`, so its 794 // The hash is taken outside the shell's own binding of `shell-file-name`, so its
767 // variables are written for the user's shell whatever the block's language. 795 // variables are written for the user's shell whatever the block's language.
768 let expanded = try languageExpansion(body, language: language, tables: tables, params: params) 796 let expanded = try languageExpansion(body, language: language, tables: tables, params: params,
797 properties: { inheritedProperty($0, heading: heading, model: model) })
769 ?? expandedBody(language: language, vars: tables.vars, body: body, shell: options.shell, params: params) 798 ?? expandedBody(language: language, vars: tables.vars, body: body, shell: options.shell, params: params)
770 let input = try cacheInput(language: language, params: params, tables: tables, expanded: expanded, call: call) 799 let input = try cacheInput(language: language, params: params, tables: tables, expanded: expanded, call: call)
771 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined() 800 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
@@ -932,7 +961,7 @@ public enum Babel {
932 /// `defaults` go under the properties, as `org-babel-default-inline-header-args` do. 961 /// `defaults` go under the properties, as `org-babel-default-inline-header-args` do.
933 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [], 962 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [],
934 inline: Bool = false) throws { 963 inline: Bool = false) throws {
935 var layers: [[(String, String)]] = [defaults] 964 var layers: [[(String, String)]] = [defaults, Babel.languageDefaults[block.language ?? ""] ?? []]
936 layers += Params.propertyLayers(language: block.language ?? "", heading: block.heading, model: model) 965 layers += Params.propertyLayers(language: block.language ?? "", heading: block.heading, model: model)
937 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0))) 966 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
938 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) { 967 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
@@ -1187,7 +1216,7 @@ public enum Babel {
1187 if params.single[":cache"] == "yes", let language = block.language { 1216 if params.single[":cache"] == "yes", let language = block.language {
1188 let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params) 1217 let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params)
1189 let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block) 1218 let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block)
1190 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text, options: refs.options) 1219 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text, options: refs.options, model: model)
1191 if state.current { 1220 if state.current {
1192 if let value = state.value { return value } 1221 if let value = state.value { return value }
1193 useCache = false 1222 useCache = false
@@ -1524,6 +1553,8 @@ public enum Babel {
1524 } 1553 }
1525 let printed = object["S"] as? String ?? "" 1554 let printed = object["S"] as? String ?? ""
1526 return printed == "nil" ? .none : .string(printed) 1555 return printed == "nil" ? .none : .string(printed)
1556 case .compiled:
1557 return compiledValue(finish, output, scalar: scalar)
1527 } 1558 }
1528 } 1559 }
1529 1560
@@ -1537,6 +1568,7 @@ public enum Babel {
1537 case .shell(let valueIsExitStatus): 1568 case .shell(let valueIsExitStatus):
1538 return valueIsExitStatus ? output.stdout.split(separator: "\n").last.map(String.init) ?? "" : output.stdout 1569 return valueIsExitStatus ? output.stdout.split(separator: "\n").last.map(String.init) ?? "" : output.stdout
1539 case .generic: return output.stdout 1570 case .generic: return output.stdout
1571 case .compiled: return compiledResult(finish, output)
1540 } 1572 }
1541 } 1573 }
1542 1574
@@ -1596,10 +1628,11 @@ public enum Babel {
1596 return fields 1628 return fields
1597 } 1629 }
1598 1630
1599 /// `org-babel-python-table-or-string`. 1631 /// `org-babel-python-table-or-string`; with `none` "null" and `braces`,
1600 static func pythonValue(_ raw: String) -> Value { 1632 /// `org-babel-java-table-or-string`, which reads `{…}` as a list too.
1633 static func pythonValue(_ raw: String, none: String = "None", braces: Bool = false) -> Value {
1601 guard let first = raw.first, let last = raw.last, raw.count >= 2, 1634 guard let first = raw.first, let last = raw.last, raw.count >= 2,
1602 (first == "[" && last == "]") || (first == "(" && last == ")") else { 1635 (first == "[" && last == "]") || (first == "(" && last == ")") || (braces && first == "{" && last == "}") else {
1603 if raw.isEmpty { return .string("") } 1636 if raw.isEmpty { return .string("") }
1604 if raw.count > 2, (raw.hasPrefix("'") && raw.hasSuffix("'")) || (raw.hasPrefix("\"") && raw.hasSuffix("\"")) { 1637 if raw.count > 2, (raw.hasPrefix("'") && raw.hasSuffix("'")) || (raw.hasPrefix("\"") && raw.hasSuffix("\"")) {
1605 return .string(String(raw.dropFirst().dropLast())) 1638 return .string(String(raw.dropFirst().dropLast()))
@@ -1608,6 +1641,7 @@ public enum Babel {
1608 return .string(raw) 1641 return .string(raw)
1609 } 1642 }
1610 var parser = PythonLiteral(Array(raw)) 1643 var parser = PythonLiteral(Array(raw))
1644 parser.braces = braces
1611 guard let parsed = parser.parse(), parser.atEnd, case .list(let items) = parsed else { return .string(raw) } 1645 guard let parsed = parser.parse(), parser.atEnd, case .list(let items) = parsed else { return .string(raw) }
1612 func text(_ v: PythonLiteral.Node) -> String { 1646 func text(_ v: PythonLiteral.Node) -> String {
1613 switch v { 1647 switch v {
@@ -1616,13 +1650,13 @@ public enum Babel {
1616 case .list(let xs): return "(" + xs.map(text).joined(separator: " ") + ")" 1650 case .list(let xs): return "(" + xs.map(text).joined(separator: " ") + ")"
1617 } 1651 }
1618 } 1652 }
1619 if items.allSatisfy({ if case .list = $0 { return true }; if case .atom("None") = $0 { return true }; return false }) { 1653 if items.allSatisfy({ if case .list = $0 { return true }; if case .atom(none) = $0 { return true }; return false }) {
1620 return .table(items.map { item -> [String]? in 1654 return .table(items.map { item -> [String]? in
1621 if case .list(let cells) = item { return cells.map(text) } 1655 if case .list(let cells) = item { return cells.map(text) }
1622 return nil 1656 return nil
1623 }) 1657 })
1624 } 1658 }
1625 return .table([items.map { if case .atom("None") = $0 { return "hline" }; return text($0) }]) 1659 return .table([items.map { if case .atom(none) = $0 { return "hline" }; return text($0) }])
1626 } 1660 }
1627 1661
1628 struct PythonLiteral { 1662 struct PythonLiteral {
@@ -1634,6 +1668,8 @@ public enum Babel {
1634 1668
1635 let chars: [Character] 1669 let chars: [Character]
1636 var i = 0 1670 var i = 0
1671 /// `{…}` is a list, as `org-babel-script-escape` reads it.
1672 var braces = false
1637 init(_ chars: [Character]) { self.chars = chars } 1673 init(_ chars: [Character]) { self.chars = chars }
1638 var atEnd: Bool { mutating get { skip(); return i == chars.count } } 1674 var atEnd: Bool { mutating get { skip(); return i == chars.count } }
1639 1675
@@ -1643,8 +1679,8 @@ public enum Babel {
1643 skip() 1679 skip()
1644 guard i < chars.count else { return nil } 1680 guard i < chars.count else { return nil }
1645 let c = chars[i] 1681 let c = chars[i]
1646 if c == "[" || c == "(" { 1682 if c == "[" || c == "(" || (braces && c == "{") {
1647 let close: Character = c == "[" ? "]" : ")" 1683 let close: Character = c == "[" ? "]" : c == "{" ? "}" : ")"
1648 i += 1 1684 i += 1
1649 var items: [Node] = [] 1685 var items: [Node] = []
1650 skip() 1686 skip()
@@ -1694,7 +1730,7 @@ public enum Babel {
1694 return .string(s) 1730 return .string(s)
1695 } 1731 }
1696 var s = "" 1732 var s = ""
1697 while i < chars.count, !",)] \n".contains(chars[i]) { 1733 while i < chars.count, !",)] \n".contains(chars[i]), !(braces && chars[i] == "}") {
1698 s.append(chars[i]) 1734 s.append(chars[i])
1699 i += 1 1735 i += 1
1700 } 1736 }
Sources/OrgCore/Compute/BabelExpand.swift +251 −12
@@ -1,18 +1,22 @@
1import Foundation 1import Foundation
2 2
3// `org-babel-expand-body:LANG` of ob-C (C, C++, cpp, D), ob-java, ob-fortran and ob-clojure 3// `org-babel-expand-body:LANG` of ob-C (C, C++, cpp, D), ob-java, ob-fortran and ob-clojure
4// (Org 9.8.7), which tangling and the `:cache` hash use. These languages don't run. 4// (Org 9.8.7), which tangling, the `:cache` hash and runs use, and their `org-babel-execute:LANG`:
5// a script that writes the expansion to a file, compiles it and runs it.
5 6
6extension Babel { 7extension Babel {
7 static let expandedLanguages: Set<String> = ["C", "C++", "cpp", "D", "java", "fortran", "clojure"] 8 static let expandedLanguages: Set<String> = ["C", "C++", "cpp", "D", "java", "fortran", "clojure"]
8 9
9 /// The body as the language's `org-babel-expand-body:` writes it, or nil for other languages. 10 /// The body as the language's `org-babel-expand-body:` writes it, or nil for other languages.
10 static func languageExpansion(_ body: String, language: String, tables: Tables, params: Params) throws -> String? { 11 /// `properties` gives inherited properties where the block is, which D's and Fortran's
12 /// expansions read; tangling reads them outside the Org buffer, so never.
13 static func languageExpansion(_ body: String, language: String, tables: Tables, params: Params,
14 properties: (String) -> String? = { _ in nil }) throws -> String? {
11 switch language { 15 switch language {
12 case "C", "C++", "cpp": return try expandC(body, d: false, tables: tables, params: params) 16 case "C", "C++", "cpp": return try expandC(body, d: false, tables: tables, params: params)
13 case "D": return try expandC(body, d: true, tables: tables, params: params) 17 case "D": return try expandC(body, d: true, tables: tables, params: params, properties: properties)
14 case "java": return try expandJava(body, vars: tables.vars, params: params) 18 case "java": return try expandJava(body, vars: tables.vars, params: params)
15 case "fortran": return try expandFortran(body, vars: tables.vars, params: params) 19 case "fortran": return try expandFortran(body, vars: tables.vars, params: params, properties: properties)
16 case "clojure": return expandClojure(body, vars: tables.vars, params: params) 20 case "clojure": return expandClojure(body, vars: tables.vars, params: params)
17 default: return nil 21 default: return nil
18 } 22 }
@@ -49,6 +53,37 @@ extension Babel {
49 return .items(items.map { $0.string ?? Elisp.printed($0) }) 53 return .items(items.map { $0.string ?? Elisp.printed($0) })
50 } 54 }
51 55
56 /// `org-babel-read` of a property value: Lisp evaluated, a quoted string read, else the
57 /// string.
58 static func propertyValue(_ value: String?) throws -> HeaderValue? {
59 guard let value else { return nil }
60 guard let first = value.first, "('`[".contains(first) else {
61 guard case .string(let s) = read(value) else { return .string(value) }
62 return .string(s)
63 }
64 guard let form = try? LispReader.readFirst(value).sexp, let result = try? Elisp().eval(form) else {
65 throw Failure.message("The property value \(value) is Lisp that only Emacs can evaluate.")
66 }
67 if let s = result.string { return .string(s) }
68 if Elisp.isNil(result) { return .items([]) }
69 guard case .list(let items) = result else { throw Failure.message("The property value \(value) isn't a list of names.") }
70 return .items(items.map { $0.string ?? Elisp.printed($0) })
71 }
72
73 /// `(org-entry-get nil KEY t)` under `heading`, or before any: the nearest value from
74 /// `#+PROPERTY` down the outline, with `KEY+` additions.
75 static func inheritedProperty(_ key: String, heading: Int?, model: DocumentModel) -> String? {
76 var levels = [model.fileProperties]
77 if let heading { levels += (model.ancestors(of: heading) + [heading]).map { model.headings[$0].properties } }
78 var value: String?
79 for entries in levels {
80 for entry in entries where entry.key.uppercased() == key.uppercased() {
81 value = entry.additive ? [value, entry.value].compactMap { $0 }.joined(separator: " ") : entry.value
82 }
83 }
84 return value
85 }
86
52 enum BaseType { case integer, float, string } 87 enum BaseType { case integer, float, string }
53 88
54 /// `org-babel-C-val-to-base-type` and `org-babel-java-val-to-base-type`. 89 /// `org-babel-C-val-to-base-type` and `org-babel-java-val-to-base-type`.
@@ -106,7 +141,7 @@ extension Babel {
106 """ 141 """
107 142
108 /// `org-babel-C-expand-C` and `org-babel-C-expand-D`. 143 /// `org-babel-C-expand-C` and `org-babel-C-expand-D`.
109 static func expandC(_ body: String, d: Bool, tables: Tables, params: Params) throws -> String { 144 static func expandC(_ body: String, d: Bool, tables: Tables, params: Params, properties: (String) -> String? = { _ in nil }) throws -> String {
110 let vars = tables.vars 145 let vars = tables.vars
111 func typeName(_ type: BaseType?) throws -> String { 146 func typeName(_ type: BaseType?) throws -> String {
112 switch type { 147 switch type {
@@ -160,10 +195,10 @@ extension Babel {
160 var body = body 195 var body = body
161 var parts: [String] = [] 196 var parts: [String] = []
162 if d { 197 if d {
163 // `:imports` and the inherited IMPORTS property, which tangling reads outside the 198 // `:imports`, or the inherited IMPORTS property.
164 // Org buffer, so never.
165 parts.append("module mmm;") 199 parts.append("module mmm;")
166 parts.append(((try headerValue(":imports", params)?.words ?? []) + ["std.stdio", "std.conv"]).map { "import \($0);" }.joined(separator: "\n")) 200 let imports = try headerValue(":imports", params) ?? propertyValue(properties("imports"))
201 parts.append(((imports?.words ?? []) + ["std.stdio", "std.conv"]).map { "import \($0);" }.joined(separator: "\n"))
167 } else { 202 } else {
168 let includes = try headerValue(":includes", params)?.words ?? [] 203 let includes = try headerValue(":includes", params)?.words ?? []
169 parts.append(includes.map { $0.hasPrefix("<") ? "#include \($0)" : "#include \"\($0)\"" }.joined(separator: "\n")) 204 parts.append(includes.map { $0.hasPrefix("<") ? "#include \($0)" : "#include \"\($0)\"" }.joined(separator: "\n"))
@@ -326,7 +361,7 @@ extension Babel {
326 // MARK: - Fortran 361 // MARK: - Fortran
327 362
328 /// `org-babel-expand-body:fortran`. 363 /// `org-babel-expand-body:fortran`.
329 static func expandFortran(_ body: String, vars: [(String, VarValue)], params: Params) throws -> String { 364 static func expandFortran(_ body: String, vars: [(String, VarValue)], params: Params, properties: (String) -> String? = { _ in nil }) throws -> String {
330 let prologue = params.single[":prologue"] 365 let prologue = params.single[":prologue"]
331 let epilogue = params.single[":epilogue"] 366 let epilogue = params.single[":epilogue"]
332 // `%S`. 367 // `%S`.
@@ -352,7 +387,6 @@ extension Babel {
352 return "real, parameter :: \(name)(\(items.count)) = (/" + items.map(printed).joined(separator: ", ") + "/)\n" 387 return "real, parameter :: \(name)(\(items.count)) = (/" + items.map(printed).joined(separator: ", ") + "/)\n"
353 } 388 }
354 } 389 }
355 // The INCLUDES and DEFINES properties are read outside the Org buffer when tangling.
356 func items(_ value: HeaderValue?) -> [String] { 390 func items(_ value: HeaderValue?) -> [String] {
357 switch value { 391 switch value {
358 case .string(let s): [s] 392 case .string(let s): [s]
@@ -360,8 +394,8 @@ extension Babel {
360 case nil: [] 394 case nil: []
361 } 395 }
362 } 396 }
363 let includes = items(try headerValue(":includes", params)) 397 let includes = items(try headerValue(":includes", params) ?? propertyValue(properties("includes")))
364 let defines = items(try headerValue(":defines", params)) 398 let defines = items(try headerValue(":defines", params) ?? propertyValue(properties("defines")))
365 let main: String 399 let main: String
366 if params.single[":main"] == "no" { 400 if params.single[":main"] == "no" {
367 main = (prologue.map { $0 + "\n" } ?? "") + body + (epilogue.map { "\n" + $0 + "\n" } ?? "") 401 main = (prologue.map { $0 + "\n" } ?? "") + body + (epilogue.map { "\n" + $0 + "\n" } ?? "")
@@ -398,4 +432,209 @@ extension Babel {
398 : "(prn " 432 : "(prn "
399 return printer + "(binding [*out* (java.io.StringWriter.)]" + expanded + "))" 433 return printer + "(binding [*out* (java.io.StringWriter.)]" + expanded + "))"
400 } 434 }
435
436 // MARK: - Running
437
438 /// A compiled language's run: `source` on the script's standard input, which the script
439 /// writes to a file, compiles and runs with a newline as standard input, as
440 /// `org-babel-eval` gives each command; the compiler's output is dropped.
441 struct CompiledRun {
442 let source: String
443 let script: String
444 let programs: [[String]]
445 }
446
447 /// `org-babel-C-execute`, `org-babel-execute:java`, `org-babel-execute:fortran` and
448 /// `org-babel-execute:clojure` with the babashka or Clojure CLI backend, with Org's
449 /// default compilers.
450 static func compiledRun(language: String, body: String, tables: Tables, params: Params, properties: (String) -> String?) throws -> CompiledRun {
451 func quoted(_ s: String) -> String { "'" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'" }
452 /// A string, or a Lisp list's items joined by spaces.
453 func joined(_ value: HeaderValue?) -> String {
454 switch value {
455 case .string(let s): s
456 case .items(let items): items.joined(separator: " ")
457 case nil: ""
458 }
459 }
460 func command(_ s: String) -> String { String(s.prefix { !$0.isWhitespace }) }
461 let expanded = try languageExpansion(body, language: language, tables: tables, params: params, properties: properties)!
462 let cmdline = params.single[":cmdline"].map { " " + $0 } ?? ""
463 let flags = joined(try headerValue(":flags", params))
464 let start = "d=$(mktemp -d) || exit 1\n"
465 let end = "s=$?\nrm -rf \"$d\"\nexit $s\n"
466 switch language {
467 case "C", "C++", "cpp":
468 let (compiler, ext) = language == "C" ? ("gcc", "c") : ("g++", "cpp")
469 let libs = joined(try headerValue(":libs", params) ?? propertyValue(properties("libs")))
470 let script = start + "cat > \"$d/C-src.\(ext)\"\necho | \(compiler) -o \"$d/C-bin\" \(flags) \"$d/C-src.\(ext)\" \(libs) > /dev/null\n"
471 + "echo | \"$d/C-bin\"\(cmdline)\n" + end
472 return CompiledRun(source: expanded, script: script, programs: [[compiler]])
473 case "D":
474 // rdmd compiles and runs.
475 let script = start + "cat > \"$d/C-src.d\"\necho | rdmd \(flags) \"$d/C-src.d\"\(cmdline)\n" + end
476 return CompiledRun(source: expanded, script: script, programs: [["rdmd"]])
477 case "fortran":
478 let script = start + "cat > \"$d/fortran-src.F90\"\necho | gfortran -o \"$d/fortran-bin\" \(flags) \"$d/fortran-src.F90\" > /dev/null\n"
479 + "echo | \"$d/fortran-bin\"\(cmdline)\n" + end
480 return CompiledRun(source: expanded, script: script, programs: [["gfortran"]])
481 case "java":
482 let javac = params.single[":javac"] ?? "javac"
483 let java = params.single[":java"] ?? "java"
484 let fullName = params.single[":classname"] ?? javaClassName(body)
485 let className = fullName.components(separatedBy: ".").last!
486 let packageName = fullName.contains(".") ? (fullName as NSString).deletingPathExtension : nil
487 // Without `:dir` (Java's default is "."), in a temporary folder without the
488 // package; else in the run's folder, under the package's folders.
489 let fromTemp = params.single[":dir"] == nil
490 let source = javaForEvaluation(expanded, suppressPackage: fromTemp, value: !params.results.contains("output"))
491 let cmpflag = params.single[":cmpflag"] ?? ""
492 let flags = params.single[":cmdline"] ?? ""
493 let cmdargs = params.single[":cmdargs"] ?? ""
494 let script: String
495 if fromTemp {
496 script = start + "cat > \"$d/\(className).java\"\n"
497 + "echo | { \(javac) \(cmpflag) \"$d/\(className).java\" && \(java) -cp \"$d/\" \(flags) \(className) \(cmdargs); }\n" + end
498 } else {
499 let folder = packageName.map { $0.replacingOccurrences(of: ".", with: "/") + "/" } ?? ""
500 let file = quoted(folder + className + ".java")
501 script = (folder.isEmpty ? "" : "mkdir -p \(quoted(folder))\n") + "cat > \(file)\n"
502 + "echo | { \(javac) \(cmpflag) \(file) && \(java) -cp \"$PWD/\" \(flags) \(fullName) \(cmdargs); }\n"
503 }
504 return CompiledRun(source: source, script: script, programs: [[command(javac)], [command(java)]])
505 case "clojure":
506 let file = "\"$d/clojure-cmd-script.clj\""
507 let run: String
508 let programs: [String]
509 switch params.single[":backend"] {
510 case nil:
511 // `org-babel-clojure-backend`: babashka when installed, else the Clojure CLI.
512 run = "if command -v bb > /dev/null 2>&1; then echo | bb \(file); else echo | clojure -M \(file); fi\n"
513 programs = ["bb", "clojure"]
514 case "babashka":
515 run = "echo | bb \(file)\n"
516 programs = ["bb"]
517 case "clojure-cli":
518 run = "echo | clojure -M \(file)\n"
519 programs = ["clojure"]
520 case let backend?:
521 throw Failure.message("The \(backend) backend for clojure needs Emacs; Orgstar runs babashka or clojure-cli.")
522 }
523 return CompiledRun(source: expanded, script: start + "cat > \(file)\n" + run + end, programs: [programs])
524 default:
525 throw Failure.message("No way to run \(language) blocks yet.")
526 }
527 }
528
529 /// `org-babel-java--result-wrapper`, writing to the result file.
530 static let javaResultWrapper = """
531
532 public static String __toString(Object val) {
533 if (val instanceof String) {
534 return "\\"" + val + "\\"";
535 } else if (val == null) {
536 return "null";
537 } else if (val.getClass().isArray()) {
538 StringBuffer sb = new StringBuffer();
539 Object[] vals = (Object[])val;
540 sb.append("[");
541 for (int ii=0; ii<vals.length; ii++) {
542 sb.append(__toString(vals[ii]));
543 if (ii<vals.length-1)
544 sb.append(",");
545 }
546 sb.append("]");
547 return sb.toString();
548 } else if (val instanceof List) {
549 StringBuffer sb = new StringBuffer();
550 List vals = (List)val;
551 sb.append("[");
552 for (int ii=0; ii<vals.size(); ii++) {
553 sb.append(__toString(vals.get(ii)));
554 if (ii<vals.size()-1)
555 sb.append(",");
556 }
557 sb.append("]");
558 return sb.toString();
559 } else {
560 return String.valueOf(val);
561 }
562 }
563
564 public static void main(String[] args) throws IOException {
565 BufferedWriter output = new BufferedWriter(new FileWriter("\(resultFileToken)"));
566 output.write(__toString(_main(args)));
567 output.close();
568 }
569 """
570
571 /// `org-babel-java--expand-for-evaluation`: without the package when run from a temporary
572 /// folder, a `main` method when there is none, the value written to the result file, and
573 /// the imports those need.
574 static func javaForEvaluation(_ body: String, suppressPackage: Bool, value: Bool) -> String {
575 let buffer = NSMutableString(string: body)
576 func search(_ regex: NSRegularExpression, from point: Int) -> NSTextCheckingResult? {
577 regex.firstMatch(in: buffer as String, options: [.withTransparentBounds, .withoutAnchoringBounds],
578 range: NSRange(location: point, length: buffer.length - point))
579 }
580 func movePast(_ regex: NSRegularExpression) -> Int {
581 var point = 0
582 while let m = search(regex, from: point) { point = min(NSMaxRange(m.range) + 1, buffer.length) }
583 return point
584 }
585 if suppressPackage, let m = search(javaPackage, from: 0) { buffer.replaceCharacters(in: m.range, with: "") }
586 if search(javaMain, from: 0) == nil {
587 buffer.insert("\n public static void main(String[] args) {\n System.out.print(\"success\");\n }\n\n", at: movePast(javaClass))
588 }
589 if value {
590 let point = movePast(javaClass)
591 buffer.insert(javaResultWrapper, at: point)
592 let after = point + javaResultWrapper.utf16.count
593 let main = buffer.range(of: "public static void main(", options: .caseInsensitive, range: NSRange(location: after, length: buffer.length - after))
594 if main.location != NSNotFound { buffer.replaceCharacters(in: main, with: "public static Object _main(") }
595 }
596 // `org-babel-java--import-maybe`, with `case-fold-search`.
597 for (package, name) in [("java.util", "List"), ("java.util", "Arrays"), ("java.io", "BufferedWriter"), ("java.io", "FileWriter"), ("java.io", "IOException")] {
598 let used = buffer.range(of: name, options: .caseInsensitive).location != NSNotFound
599 let imported = buffer.range(of: "(?im)^import .*\(package).*(?:\\*|\(name));", options: .regularExpression).location != NSNotFound
600 if used, !imported { buffer.insert("import \(package).\(name);\n", at: movePast(javaPackage)) }
601 }
602 return buffer as String
603 }
604
605 /// A compiled language's result before it is read: standard output without its common
606 /// indentation, trimmed for Fortran; Java's value from the result file; Clojure's as printed.
607 static func compiledResult(_ finish: BabelJob.Finish, _ output: BabelOutput) -> String {
608 switch finish.language {
609 case "java": finish.output ? output.stdout : output.resultFile ?? ""
610 case "clojure": output.stdout
611 case "fortran": Tangle.removeIndentation(output.stdout).trimmingCharacters(in: .whitespacesAndNewlines)
612 default: Tangle.removeIndentation(output.stdout)
613 }
614 }
615
616 /// `org-babel-result-cond` of the languages: a string as it is, or with `org-babel-read`
617 /// for Fortran; else a table from the output (`org-babel-import-elisp-from-file`), or for
618 /// Java and Clojure `org-babel-script-escape`.
619 static func compiledValue(_ finish: BabelJob.Finish, _ output: BabelOutput, scalar: Bool) -> Value {
620 let raw = compiledResult(finish, output)
621 switch finish.language {
622 case "java": return scalar ? .string(raw) : pythonValue(raw, none: "null", braces: true)
623 case "clojure": return scalar ? .string(raw) : read(raw)
624 case "fortran": return scalar ? read(raw) : importTable(raw)
625 default: return scalar ? .string(raw) : importTable(raw)
626 }
627 }
628
629 /// `org-babel-read` of printed output: a number, or a single string's contents; other
630 /// values as printed. Clojure prints a final newline, so `org-babel-script-escape` never
631 /// takes its output for a list.
632 static func read(_ raw: String) -> Value {
633 let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
634 if let n = number(trimmed) { return .string(n) }
635 if trimmed.hasPrefix("\""), let (sexp, length) = try? LispReader.readFirst(trimmed), length == trimmed.count, let s = sexp.string {
636 return .string(s)
637 }
638 return .string(raw)
639 }
401} 640}
Tests/OrgAppTests/BabelSessionTests.swift +34
@@ -30,3 +30,37 @@ struct BabelSessionStateTests {
30 #expect(try await run("#+begin_src python :session py\n[a, a]\n#+end_src\n", in: folder) == "\n#+RESULTS:\n| 2 | 2 |\n") 30 #expect(try await run("#+begin_src python :session py\n[a, a]\n#+end_src\n", in: folder) == "\n#+RESULTS:\n| 2 | 2 |\n")
31 } 31 }
32} 32}
33
34struct BabelRunnerTests {
35 @Test func refusesWithoutTheCompiler() async throws {
36 for (text, programs, message) in [
37 ("#+begin_src D\nwriteln(1);\n#+end_src\n", ["rdmd"], "rdmd isn't installed, so D blocks can't run."),
38 ("#+begin_src clojure\n1\n#+end_src\n", ["bb", "clojure"], "Neither bb nor clojure is installed, so clojure blocks can't run."),
39 ] where !programs.contains(where: BabelRunner.installed) {
40 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
41 guard case .success(let job) = Babel.plan(context) else {
42 Issue.record("not planned: \(text)")
43 continue
44 }
45 do {
46 _ = try await BabelRunner.run(job, fileDirectory: FileManager.default.temporaryDirectory)
47 Issue.record("ran: \(text)")
48 } catch let failure as BabelRunner.Failure {
49 #expect(failure.description == message)
50 }
51 }
52 }
53
54 @Test(.enabled(if: BabelRunner.installed("gcc")))
55 func compilesAndRunsC() async throws {
56 let text = "#+begin_src C :includes <stdio.h>\nprintf(\"%d\\n\", 6 * 7);\n#+end_src\n"
57 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
58 guard case .success(let job) = Babel.plan(context) else {
59 Issue.record("not planned")
60 return
61 }
62 let output = try await BabelRunner.run(job, fileDirectory: FileManager.default.temporaryDirectory)
63 #expect(output.stdout == "42\n")
64 #expect(output.problem == nil)
65 }
66}
Tests/OrgCoreTests/BabelCacheTests.swift +8 −3
@@ -121,7 +121,7 @@ struct BabelCacheTests {
121 #expect(job.directory == (emacs[0] as NSString).deletingLastPathComponent + "/") 121 #expect(job.directory == (emacs[0] as NSString).deletingLastPathComponent + "/")
122 } 122 }
123 123
124 /// Languages that don't run hash the body as their `org-babel-expand-body:LANG` writes it. 124 /// The compiled languages hash the body as their `org-babel-expand-body:LANG` writes it.
125 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) 125 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
126 func expandedLanguagesHashLikeEmacs() throws { 126 func expandedLanguagesHashLikeEmacs() throws {
127 let texts = [ 127 let texts = [
@@ -130,13 +130,18 @@ struct BabelCacheTests {
130 "#+begin_src D :cache yes :var s=\"x\"\nwriteln(s);\n#+end_src\n", 130 "#+begin_src D :cache yes :var s=\"x\"\nwriteln(s);\n#+end_src\n",
131 "#+begin_src fortran :cache yes :var n=3\nprint *, n\n#+end_src\n", 131 "#+begin_src fortran :cache yes :var n=3\nprint *, n\n#+end_src\n",
132 "#+begin_src clojure :cache yes :var n=3\n(+ n 1)\n#+end_src\n", 132 "#+begin_src clojure :cache yes :var n=3\n(+ n 1)\n#+end_src\n",
133 "#+begin_src java :cache yes\nSystem.out.println(1);\n#+end_src\n",
134 "#+begin_src java :cache yes :results value :var n=3 :classname pkg.Main :imports java.util.Map\nreturn n;\n#+end_src\n",
135 "#+begin_src java :cache yes :dir /tmp :results output silent\nSystem.out.println(1);\n#+end_src\n",
136 "* H\n:PROPERTIES:\n:IMPORTS: std.math\n:END:\n#+begin_src D :cache yes\nwriteln(sqrt(2.0));\n#+end_src\n",
137 "* H\n:PROPERTIES:\n:INCLUDES: \"x.inc\"\n:END:\n#+begin_src fortran :cache yes\nprint *, 1\n#+end_src\n",
133 ] 138 ]
134 for text in texts { 139 for text in texts {
135 let emacs = try EmacsOracle.evaluate(text, """ 140 let emacs = try EmacsOracle.evaluate(text, """
136 (progn (require 'ob-C) (require 'ob-fortran) (require 'ob-clojure) (setq org-babel-clojure-backend 'babashka) 141 (progn (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure) (setq org-babel-clojure-backend 'babashka)
137 (goto-char (point-min)) (search-forward "#+begin_src") 142 (goto-char (point-min)) (search-forward "#+begin_src")
138 (list (org-babel-sha1-hash))) 143 (list (org-babel-sha1-hash)))
139 """) 144 """, prelude: "(setq-default indent-tabs-mode nil)")
140 let cached = text + "\n#+RESULTS[\(emacs[0])]:\n: 42\n" 145 let cached = text + "\n#+RESULTS[\(emacs[0])]:\n: 42\n"
141 let caret = (cached as NSString).range(of: "#+begin_src").location + 2 146 let caret = (cached as NSString).range(of: "#+begin_src").location + 2
142 let context = EditContext(revision: 0, text: cached, tree: OrgParser.parse(cached), selection: [caret..<caret]) 147 let context = EditContext(revision: 0, text: cached, tree: OrgParser.parse(cached), selection: [caret..<caret])
Tests/OrgCoreTests/BabelCompiledTests.swift added +225
@@ -0,0 +1,225 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// C, C++, D, Java, Fortran and Clojure blocks (ob-C, ob-java, ob-fortran, ob-clojure).
6struct BabelCompiledTests {
7 static func installed(_ program: String) -> Bool {
8 (ProcessInfo.processInfo.environment["PATH"] ?? "").split(separator: ":").contains {
9 FileManager.default.isExecutableFile(atPath: "\($0)/\(program)")
10 }
11 }
12
13 static let table = "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | 2 |\n| 3 | 4 |\n\n"
14
15 /// Compiled and run here and in Emacs.
16 static let cases: [String] = [
17 "#+begin_src C :includes <stdio.h>\nprintf(\"hello\\n\");\n#+end_src\n",
18 "#+begin_src C :includes <stdio.h> :results output\nprintf(\"a b\\nc d\\n\");\n#+end_src\n",
19 "#+begin_src C :includes <stdio.h>\nprintf(\"a b\\nc d\\n\");\n#+end_src\n",
20 "#+begin_src C :includes <stdio.h>\nprintf(\"1\\t2\\n3\\t4\\n\");\n#+end_src\n",
21 "#+begin_src C :includes <stdio.h>\nprintf(\" x\\n y\\n\");\n#+end_src\n",
22 "#+begin_src C :includes <stdio.h> :results output\nprintf(\" x\\n y\\n\");\n#+end_src\n",
23 "#+begin_src C :includes <stdio.h> :results verbatim\nprintf(\"007\\n\");\n#+end_src\n",
24 "#+begin_src C :includes <stdio.h> :results raw\nprintf(\"*bold*\\n\");\n#+end_src\n",
25 "#+begin_src C :includes '(<stdio.h> <string.h>) :var n=3 x=2.5 s=\"hi\"\nprintf(\"%d %.1f %s %zu\\n\", n, x, s, strlen(s));\n#+end_src\n",
26 "#+begin_src C :includes <stdio.h> :var l='(1 2 3)\nint s = 0; for (int i = 0; i < l_cols; i++) s += l[i]; printf(\"%d\\n\", s);\n#+end_src\n",
27 table + "#+begin_src C :var t=tbl :includes <stdio.h>\nfor (int i = 0; i < t_rows; i++) printf(\"%d %d\\n\", t[i][0], t_h(i, \"b\"));\n#+end_src\n",
28 table + "#+begin_src C :var t=tbl :includes <stdio.h> :colnames yes\nfor (int i = 0; i < t_rows; i++) printf(\"%d %d\\n\", t[i][1], t[i][0]);\n#+end_src\n",
29 "#+NAME: rt\n| r1 | 1 |\n| r2 | 2 |\n\n#+begin_src C :var t=rt :includes <stdio.h> :rownames yes\nfor (int i = 0; i < t_rows; i++) printf(\"%d\\n\", t[i][0] * 10);\n#+end_src\n",
30 "#+begin_src C :includes <stdio.h> :main no\nint main() { printf(\"own main\\n\"); return 0; }\n#+end_src\n",
31 "#+begin_src C :includes <stdio.h> :main no\nint main(int argc, char **argv) { printf(\"%d\\n\", argc); return 0; }\n#+end_src\n",
32 "#+begin_src C :includes <stdio.h> :cmdline one \"two words\" :results output\nint main(int argc, char **argv) { for (int i = 1; i < argc; i++) printf(\"[%s]\\n\", argv[i]); return 0; }\n#+end_src\n",
33 "#+begin_src C :includes <stdio.h> :flags -DN=5 -O2\nprintf(\"%d\\n\", N * 2);\n#+end_src\n",
34 "#+begin_src C :includes <stdio.h> <math.h> :libs -lm\nprintf(\"%.3f\\n\", sqrt(2.0));\n#+end_src\n",
35 "#+begin_src C :includes <stdio.h> :defines N 3 M 4\nprintf(\"%d\\n\", N * M);\n#+end_src\n",
36 "#+begin_src C :includes <stdio.h> :prologue int k = 6; :epilogue printf(\"%d\\n\", k);\nk *= 7;\n#+end_src\n",
37 "#+begin_src C :includes <stdio.h> <unistd.h> :dir /tmp :results output\nchar b[1024]; getcwd(b, sizeof b); printf(\"%s\\n\", b);\n#+end_src\n",
38 "#+begin_src C :includes <stdio.h>\nint c = getchar(); printf(\"%d\\n\", c);\n#+end_src\n",
39 "#+begin_src C :includes <stdio.h> :results output\nprintf(\"out\\n\"); fprintf(stderr, \"err\\n\"); return 3;\n#+end_src\n",
40 "#+begin_src C\nsyntax error\n#+end_src\n",
41 "#+begin_src C :includes <stdio.h> :session s\nprintf(\"no session\\n\");\n#+end_src\n",
42 "#+begin_src C++ :includes <iostream> :namespaces std\ncout << \"c++ \" << 6 * 7 << endl;\n#+end_src\n",
43 "#+begin_src cpp :includes <iostream> :var v='(1.5 2.5)\nstd::cout << v[0] + v[1] << std::endl;\n#+end_src\n",
44 "#+begin_src C++ :includes <iostream> <vector> :results output :wrap example\nstd::vector<int> v{1, 2, 3}; for (int x : v) std::cout << x << \"\\n\";\n#+end_src\n",
45 "* Heading\n:PROPERTIES:\n:libs: -lm\n:END:\n#+begin_src C :includes <stdio.h> <math.h>\nprintf(\"%.0f\\n\", pow(2, 10));\n#+end_src\n",
46 "#+NAME: words\n| x | yy |\n| zzz | w |\n\n#+begin_src C :var t=words :includes <stdio.h> <string.h>\nprintf(\"%s %zu\\n\", t[1][0], strlen(t[0][1]));\n#+end_src\n",
47 "#+begin_src C :includes <stdio.h> :results output list\nprintf(\"a\\nb\\n\");\n#+end_src\n",
48 "#+begin_src C :includes <stdio.h> :var x=gen\nprintf(\"%d\\n\", x + 1);\n#+end_src\n\n#+NAME: gen\n#+begin_src C :includes <stdio.h>\nprintf(\"41\\n\");\n#+end_src\n",
49 ]
50
51 static let emacsSetup = """
52 (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure)
53 (setq org-confirm-babel-evaluate nil org-babel-clojure-backend 'babashka)
54 """
55
56 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil && installed("gcc") && installed("g++")))
57 func compiledMatchEmacs() throws {
58 let folder = FileManager.default.temporaryDirectory
59 var emacsCases: [EmacsOracle.Case] = []
60 var ours: [(text: String, failure: String?)] = []
61 for text in Self.cases {
62 let caret = (text as NSString).range(of: "#+begin_src").location + 2
63 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder))
64 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
65 (progn \(Self.emacsSetup)
66 (setq default-directory temporary-file-directory)
67 (org-babel-execute-src-block))
68 """))
69 }
70 let emacs = try EmacsOracle.run(emacsCases)
71 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
72 #expect(mine.text == theirs.text, "case \(i):\n\(Self.cases[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
73 }
74 }
75
76 /// Blocks whose toolchain may not be installed, with the output the program would give:
77 /// the source each writes and the result each inserts, compared with Emacs's.
78 static let canned: [(text: String, output: String)] = [
79 ("#+begin_src D :var n=3\nwriteln(n);\n#+end_src\n", "3\n"),
80 ("#+begin_src D :imports std.math :var x=2.0\nwriteln(sqrt(x));\n#+end_src\n", "1.41421\n"),
81 (table + "#+begin_src D :var t=tbl\nforeach (r; t) writeln(r[0], \" \", t_h(0, \"b\"));\n#+end_src\n", "1 2\n3 2\n"),
82 ("* H\n:PROPERTIES:\n:IMPORTS: std.range\n:END:\n#+begin_src D :cmdline x y :flags -O\nwriteln(iota(3));\n#+end_src\n", "[0, 1, 2]\n"),
83 ("#+begin_src java\nSystem.out.println(\"hi\");\n#+end_src\n", "hi\n"),
84 ("#+begin_src java :results output\nSystem.out.println(\"a b\");\nSystem.out.println(\"c d\");\n#+end_src\n", "a b\nc d\n"),
85 ("#+begin_src java :results value\nreturn 42;\n#+end_src\n", "42"),
86 ("#+begin_src java :results value\nreturn \"hello\";\n#+end_src\n", "\"hello\""),
87 ("#+begin_src java :results value\nreturn Arrays.asList(1, 2, 3);\n#+end_src\n", "[1,2,3]"),
88 ("#+begin_src java :results value\nreturn Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4));\n#+end_src\n", "[[1,2],[3,4]]"),
89 ("#+begin_src java :results value\nreturn new Object[] {1, null};\n#+end_src\n", "[1,null]"),
90 ("#+begin_src java :results value\nreturn new Object[] {new Object[] {\"x\", 1}, null, new Object[] {\"y\", 2}};\n#+end_src\n", "[[\"x\",1],null,[\"y\",2]]"),
91 ("#+begin_src java :results value verbatim\nreturn Arrays.asList(1, 2);\n#+end_src\n", "[1,2]"),
92 (table + "#+begin_src java :var t=tbl :results value\nreturn t.get(1).get(0);\n#+end_src\n", "3"),
93 ("#+begin_src java :var n=3 s=\"x\" x=1.5 :imports java.util.ArrayList\nSystem.out.println(n + s + x);\n#+end_src\n", "3x1.5\n"),
94 ("#+begin_src java :classname pkg.Hello\nSystem.out.println(\"pkg\");\n#+end_src\n", "pkg\n"),
95 ("#+begin_src java\npackage org.ex;\n\npublic class Thing {\n public static void main(String[] args) {\n System.out.println(\"thing\");\n }\n}\n#+end_src\n", "thing\n"),
96 ("#+begin_src java :results value\npublic class Calc {\n static int twice(int x) {\n return x * 2;\n }\n public static void main(String[] args) {\n return twice(21);\n }\n}\n#+end_src\n", "42"),
97 ("#+begin_src java\npublic class NoMain {\n static int x = 1;\n}\n#+end_src\n", "success"),
98 ("#+begin_src java :dir /tmp :cmdline -ea :cmdargs one two :cmpflag -g\nSystem.out.println(args.length);\n#+end_src\n", "2\n"),
99 ("#+begin_src fortran\nprint *, \"hello\"\n#+end_src\n", " hello\n"),
100 ("#+begin_src fortran :var n=3\nprint *, n * 14\n#+end_src\n", " 42\n"),
101 ("#+begin_src fortran :results output\nprint *, 1, 2\nprint *, 3, 4\n#+end_src\n", " 1 2\n 3 4\n"),
102 ("#+begin_src fortran\nprint *, 1, 2\nprint *, 3, 4\n#+end_src\n", " 1 2\n 3 4\n"),
103 ("#+begin_src fortran :results verbatim\nprint *, \"007\"\n#+end_src\n", " 007\n"),
104 ("#+begin_src fortran :var x=2.5 s=\"ab\" :flags -O2 :cmdline q\nprint *, x, s\n#+end_src\n", " 2.50000000 ab\n"),
105 (table + "#+begin_src fortran :var t=tbl\nprint *, t(2,1)\n#+end_src\n", " 3.00000000 \n"),
106 ("#+begin_src fortran :main no\nprogram p\nprint *, 'p'\nend program p\n#+end_src\n", " p\n"),
107 ("#+begin_src fortran :includes \"x.inc\" :defines N 2\nprint *, N\n#+end_src\n", " 2\n"),
108 ("* H\n:PROPERTIES:\n:INCLUDES: \"y.inc\"\n:END:\n#+begin_src fortran\nprint *, 1\n#+end_src\n", " 1\n"),
109 ("#+begin_src clojure\n(+ 40 2)\n#+end_src\n", "42\n"),
110 ("#+begin_src clojure\n(str \"a\" \"b\")\n#+end_src\n", "\"ab\"\n"),
111 ("#+begin_src clojure\n[1 2 3]\n#+end_src\n", "[1 2 3]\n"),
112 ("#+begin_src clojure\n'(1 2)\n#+end_src\n", "(1 2)\n"),
113 ("#+begin_src clojure\n{:a 1}\n#+end_src\n", "{:a 1}\n"),
114 ("#+begin_src clojure\nnil\n#+end_src\n", "nil\n"),
115 ("#+begin_src clojure :results output\n(println \"a\")\n(println \"b\")\n#+end_src\n", "a\nb\n"),
116 ("#+begin_src clojure :var x=3 l='(1 2)\n;; comment\n(apply + x l)\n#+end_src\n", "6\n"),
117 ("#+begin_src clojure :results pp\n{:a 1 :b 2}\n#+end_src\n", "{:a 1, :b 2}\n"),
118 ("#+begin_src clojure :ns demo :results verbatim\n(str *ns*)\n#+end_src\n", "\"demo\"\n"),
119 (table + "#+begin_src clojure :var t=tbl\n(count t)\n#+end_src\n", "2\n"),
120 ]
121
122 static let sourceMark = "\n-----SOURCE-----\n"
123
124 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
125 func cannedMatchEmacs() throws {
126 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-compiled-\(UUID().uuidString)")
127 try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
128 defer { try? FileManager.default.removeItem(at: folder) }
129 var emacsCases: [EmacsOracle.Case] = []
130 var ours: [String] = []
131 for (text, output) in Self.canned {
132 let caret = (text as NSString).range(of: "#+begin_src").location + 2
133 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
134 switch Babel.plan(context) {
135 case .failure(.message(let message)):
136 ours.append("failed: \(message)")
137 case .success(let job):
138 guard case .success(let edits) = Babel.finish(job, output: BabelOutput(stdout: output, resultFile: output), text: text) else {
139 ours.append("not finished")
140 break
141 }
142 let new = edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
143 ours.append(new + Self.sourceMark + job.input)
144 }
145 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
146 (let (source)
147 \(Self.emacsSetup)
148 (setq default-directory \(Babel.lispString(folder.path + "/")))
149 (advice-add 'org-babel-eval :override (lambda (&rest _) \(Babel.lispString(output))) '((name . canned)))
150 (advice-add 'org-babel-eval-read-file :override (lambda (&rest _) \(Babel.lispString(output))) '((name . canned)))
151 (advice-add 'write-region :before
152 (lambda (start end file &rest _)
153 (when (string-match-p "\\\\(C-src-\\\\|fortran-src-\\\\|clojure-cmd-script-\\\\|\\\\.java\\\\'\\\\)" file)
154 (setq source (cond ((stringp start) start) (start (buffer-substring-no-properties start end)) (t (buffer-string))))))
155 '((name . capture)))
156 (unwind-protect (org-babel-execute-src-block)
157 (advice-remove 'org-babel-eval 'canned)
158 (advice-remove 'org-babel-eval-read-file 'canned)
159 (advice-remove 'write-region 'capture))
160 (goto-char (point-max))
161 (insert \(Babel.lispString(Self.sourceMark))
162 (replace-regexp-in-string "FileWriter(\\"[^\\"]*\\")" \(Babel.lispString("FileWriter(\"\(Babel.resultFileToken)\")")) (or source "") t t)))
163 """))
164 }
165 let emacs = try EmacsOracle.run(emacsCases)
166 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
167 #expect(mine == theirs.text, "case \(i):\n\(Self.canned[i].text)\nours:\n\(mine)\nemacs (\(theirs.error)):\n\(theirs.text)")
168 }
169 }
170
171 static func job(_ text: String) -> BabelJob? {
172 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
173 guard case .success(let job) = Babel.plan(context) else { return nil }
174 return job
175 }
176
177 @Test func plansCompileAndRun() throws {
178 let c = try #require(Self.job("#+begin_src C :flags -O2 :libs -lm :cmdline a b\nreturn 0;\n#+end_src\n"))
179 #expect(c.runner == .process(executable: "/bin/sh", arguments: [Babel.scriptFileToken]))
180 #expect(c.programs == [["gcc"]])
181 #expect(c.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/C-src.c\"\necho | gcc -o \"$d/C-bin\" -O2 \"$d/C-src.c\" -lm > /dev/null\necho | \"$d/C-bin\" a b\ns=$?\nrm -rf \"$d\"\nexit $s\n")
182 #expect(c.input == "\n\n\n\n\n\n\nint main() {\nreturn 0;\nreturn 0;\n}\n\n\n")
183 let cpp = try #require(Self.job("#+begin_src C++\nreturn 0;\n#+end_src\n"))
184 #expect(cpp.programs == [["g++"]])
185 #expect(cpp.script?.contains("echo | g++ -o \"$d/C-bin\" \"$d/C-src.cpp\" > /dev/null\n") == true)
186
187 let d = try #require(Self.job("#+begin_src D :flags -O :cmdline x\nwriteln(1);\n#+end_src\n"))
188 #expect(d.programs == [["rdmd"]])
189 #expect(d.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/C-src.d\"\necho | rdmd -O \"$d/C-src.d\" x\ns=$?\nrm -rf \"$d\"\nexit $s\n")
190
191 let fortran = try #require(Self.job("#+begin_src fortran :flags -O2 :cmdline q\nprint *, 1\n#+end_src\n"))
192 #expect(fortran.programs == [["gfortran"]])
193 #expect(fortran.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/fortran-src.F90\"\necho | gfortran -o \"$d/fortran-bin\" -O2 \"$d/fortran-src.F90\" > /dev/null\n"
194 + "echo | \"$d/fortran-bin\" q\ns=$?\nrm -rf \"$d\"\nexit $s\n")
195
196 // Java's default `:dir .`: compiled in the run's folder, under the package's folders.
197 let java = try #require(Self.job("#+begin_src java :classname org.ex.Hello :cmpflag -g :cmdline -ea :cmdargs 1 2\nSystem.out.println(1);\n#+end_src\n"))
198 #expect(java.programs == [["javac"], ["java"]])
199 #expect(java.directory == ".")
200 #expect(java.finish.output)
201 #expect(java.script == "mkdir -p 'org/ex/'\ncat > 'org/ex/Hello.java'\necho | { javac -g 'org/ex/Hello.java' && java -cp \"$PWD/\" -ea org.ex.Hello 1 2; }\n")
202 #expect(java.input.hasPrefix("package org.ex;\n"))
203 let value = try #require(Self.job("#+begin_src java :results value :javac javac -Xlint :java /opt/java -server\nreturn 1;\n#+end_src\n"))
204 #expect(value.programs == [["javac"], ["/opt/java"]])
205 #expect(value.input.contains("new FileWriter(\"\(Babel.resultFileToken)\")"))
206 #expect(value.input.contains("public static Object _main(String[] args) {"))
207 #expect(value.script?.contains("echo | { javac -Xlint 'Main.java' && /opt/java -server -cp \"$PWD/\" Main ; }") == true)
208
209 let clojure = try #require(Self.job("#+begin_src clojure\n(+ 1 2)\n#+end_src\n"))
210 #expect(clojure.programs == [["bb", "clojure"]])
211 #expect(clojure.input == "(prn (binding [*out* (java.io.StringWriter.)](+ 1 2)))")
212 #expect(clojure.script == "d=$(mktemp -d) || exit 1\ncat > \"$d/clojure-cmd-script.clj\"\n"
213 + "if command -v bb > /dev/null 2>&1; then echo | bb \"$d/clojure-cmd-script.clj\"; else echo | clojure -M \"$d/clojure-cmd-script.clj\"; fi\n"
214 + "s=$?\nrm -rf \"$d\"\nexit $s\n")
215 let cli = try #require(Self.job("#+begin_src clojure :backend clojure-cli\n1\n#+end_src\n"))
216 #expect(cli.programs == [["clojure"]])
217 #expect(cli.script?.contains("echo | clojure -M \"$d/clojure-cmd-script.clj\"\n") == true)
218 let cider = "#+begin_src clojure :backend cider\n1\n#+end_src\n"
219 guard case .failure(.message(let message)) = Babel.plan(EditContext(revision: 0, text: cider, tree: OrgParser.parse(cider), selection: [2..<2])) else {
220 Issue.record("planned cider")
221 return
222 }
223 #expect(message == "The cider backend for clojure needs Emacs; Orgstar runs babashka or clojure-cli.")
224 }
225}