Commit f984b02522
Verified · cmc
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 @@ | |||
| 1 | import Foundation | 1 | import 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 | ||
| 6 | extension Babel { | 7 | extension 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 | |||
| 34 | struct 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 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import OrgCore | ||
| 4 | |||
| 5 | /// C, C++, D, Java, Fortran and Clojure blocks (ob-C, ob-java, ob-fortran, ob-clojure). | ||
| 6 | struct 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 | } | ||