krz/orgstar

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

Sources/OrgCore/Compute/BabelExpand.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Sources/OrgCore/Compute/BabelExpand.swift history · blame · raw

640 lines · 34693 bytes

  1import Foundation
  2
  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, 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.
  6
  7extension Babel {
  8    static let expandedLanguages: Set<String> = ["C", "C++", "cpp", "D", "java", "fortran", "clojure"]
  9
 10    /// The body as the language's `org-babel-expand-body:` writes it, or nil for other languages.
 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? {
 15        switch language {
 16        case "C", "C++", "cpp": return try expandC(body, d: false, tables: tables, params: params)
 17        case "D": return try expandC(body, d: true, tables: tables, params: params, properties: properties)
 18        case "java": return try expandJava(body, vars: tables.vars, params: params)
 19        case "fortran": return try expandFortran(body, vars: tables.vars, params: params, properties: properties)
 20        case "clojure": return expandClojure(body, vars: tables.vars, params: params)
 21        default: return nil
 22        }
 23    }
 24
 25    enum HeaderValue {
 26        case string(String)
 27        case items([String])
 28
 29        /// `split-string` of a string, or the items.
 30        var words: [String] {
 31            switch self {
 32            case .string(let s): s.split(whereSeparator: \.isWhitespace).map(String.init)
 33            case .items(let items): items
 34            }
 35        }
 36    }
 37
 38    /// A header value as `org-babel-read` gives it: a string, or the items of a Lisp list;
 39    /// nil when absent.
 40    static func headerValue(_ key: String, _ params: Params) throws -> HeaderValue? {
 41        guard let raw = params.single[key] else { return nil }
 42        var value = params.evaluated[key]
 43        if value == nil, params.lisp.contains(key) {
 44            guard let form = try? LispReader.readFirst(raw).sexp, let result = try? Elisp().eval(form) else {
 45                throw Failure.message("\(key) \(raw) is Lisp that only Emacs can evaluate.")
 46            }
 47            value = result
 48        }
 49        guard let value else { return .string(raw) }
 50        if let s = value.string { return .string(s) }
 51        if Elisp.isNil(value) { return .items([]) }
 52        guard case .list(let items) = value else { throw Failure.message("\(key) \(raw) isn't a list of names.") }
 53        return .items(items.map { $0.string ?? Elisp.printed($0) })
 54    }
 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
 87    enum BaseType { case integer, float, string }
 88
 89    /// `org-babel-C-val-to-base-type` and `org-babel-java-val-to-base-type`.
 90    static func baseType(_ cells: [Cell]) -> BaseType? {
 91        var type: BaseType?
 92        for cell in cells {
 93            switch cell {
 94            case .text: type = .string
 95            case .number(let n) where n.contains(".") || n.contains("e") || n.contains("N"):
 96                if type != .string { type = .float }
 97            case .number:
 98                if type == nil { type = .integer }
 99            }
100        }
101        return type
102    }
103
104    static func cells(_ value: VarValue) throws -> [Cell] {
105        switch value {
106        case .scalar(let s): return [.text(s)]
107        case .number(let n): return [.number(n)]
108        case .list(let items): return items
109        case .table(let rows):
110            guard !rows.contains(where: { $0 == nil }) else { throw Failure.message("Tables with rules in :var aren't supported for this language yet.") }
111            return rows.flatMap { $0! }
112        }
113    }
114
115    // MARK: - C, C++ and D
116
117    static let cColumnUtility = """
118
119        #ifndef _STRING_H
120        #include <string.h>
121        #endif
122        int get_column_num (int nbcols, const char** header, const char* column)
123        {
124          int c;
125          for (c=0; c<nbcols; c++)
126            if (strcmp(header[c],column)==0)
127              return c;
128          return -1;
129        }
130
131        """
132    static let dColumnUtility = """
133        int get_column_num (string[] header, string column)
134        {
135          foreach (c, h; header)
136            if (h==column)
137              return to!int(c);
138          return -1;
139        }
140
141        """
142
143    /// `org-babel-C-expand-C` and `org-babel-C-expand-D`.
144    static func expandC(_ body: String, d: Bool, tables: Tables, params: Params, properties: (String) -> String? = { _ in nil }) throws -> String {
145        let vars = tables.vars
146        func typeName(_ type: BaseType?) throws -> String {
147            switch type {
148            case .integer: return "int"
149            case .float: return "double"
150            case .string: return d ? "string" : "const char*"
151            case nil: throw Failure.message("A :var value has no type C can take.")
152            }
153        }
154        func literal(_ cell: Cell, _ type: BaseType) -> String {
155            type == .string ? "\"" + cellText(cell) + "\"" : cellText(cell)
156        }
157        // `org-babel-C-var-to-C`.
158        let declarations = try vars.map { name, value -> String in
159            let type = try baseType(cells(value))
160            let typeName = try typeName(type)
161            var suffix = ""
162            let data: String
163            switch value {
164            case .scalar, .number:
165                data = literal(try cells(value)[0], type!)
166            case .list(let items):
167                suffix = "[\(items.count)]"
168                data = (d ? "[" : "{") + items.map { literal($0, type!) }.joined(separator: ",") + (d ? "]" : "}")
169            case .table(let rows):
170                let rows = rows.map { $0! }
171                suffix = d ? "[\(rows.first?.count ?? 0)][\(rows.count)]" : "[\(rows.count)][\(rows.first?.count ?? 0)]"
172                data = (d ? "[\n" : "{\n")
173                    + rows.map { (d ? " [" : " {") + $0.map { literal($0, type!) }.joined(separator: ",") + (d ? "]" : "}") }.joined(separator: ",\n")
174                    + (d ? "\n]" : "\n}")
175            }
176            return d ? "\(typeName)\(suffix) \(name) = \(data);" : "\(typeName) \(name)\(suffix) = \(data);"
177        }
178        // `org-babel-C-table-sizes-to-C`.
179        let sizes = vars.map { name, value -> String in
180            switch value {
181            case .table(let rows): "const int \(name)_rows = \(rows.count);\nconst int \(name)_cols = \(rows.first??.count ?? 0);"
182            case .list(let items): "const int \(name)_cols = \(items.count);"
183            case .scalar, .number: ""
184            }
185        }
186        // `org-babel-C-header-to-C`.
187        let headers = try tables.colnames.map { table, names -> String in
188            let value = vars.first { $0.0 == table }?.1
189            let typeName = try typeName(try value.map(cells).flatMap(baseType))
190            let quoted = names.map { "\"" + cellText($0) + "\"" }.joined(separator: ",")
191            return d
192                ? "string[\(names.count)] \(table)_header = [\(quoted)];\n\(typeName) \(table)_h (size_t row, string col) { return \(table)[row][get_column_num(\(table)_header,col)]; }"
193                : "const char* \(table)_header[\(names.count)] = {\(quoted)};\n\(typeName) \(table)_h (int row, const char* col) { return \(table)[row][get_column_num(\(names.count),\(table)_header,col)]; }"
194        }
195        var body = body
196        var parts: [String] = []
197        if d {
198            // `:imports`, or the inherited IMPORTS property.
199            parts.append("module mmm;")
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"))
202        } else {
203            let includes = try headerValue(":includes", params)?.words ?? []
204            parts.append(includes.map { $0.hasPrefix("<") ? "#include \($0)" : "#include \"\($0)\"" }.joined(separator: "\n"))
205            var defines: [String] = []
206            switch try headerValue(":defines", params) {
207            case .string(let s):
208                let words = HeaderValue.string(s).words
209                defines = stride(from: 0, to: words.count - 1, by: 2).map { words[$0] + " " + words[$0 + 1] }
210            case .items(let items): defines = items
211            case nil: break
212            }
213            parts.append(defines.map { "#define \($0)" }.joined(separator: "\n"))
214            parts.append((try headerValue(":namespaces", params)?.words ?? []).map { "using namespace \($0);" }.joined(separator: "\n"))
215            body = (params.single[":prologue"].map { $0 + "\n" } ?? "") + body + (params.single[":epilogue"].map { "\n" + $0 + "\n" } ?? "")
216        }
217        parts.append(declarations.joined(separator: "\n"))
218        parts.append(sizes.joined(separator: "\n"))
219        parts.append(tables.colnames.isEmpty ? "" : d ? dColumnUtility : cColumnUtility)
220        parts.append(headers.joined(separator: "\n"))
221        // `org-babel-C-ensure-main-wrap`.
222        let hasMain = body.range(of: "(?im)^[ \\t]*[intvod]+[ \\t\\n\\r]*main[ \\t]*\\(.*\\)", options: .regularExpression) != nil
223        parts.append(params.single[":main"] == "no" || hasMain ? body : "int main() {\n\(body)\nreturn 0;\n}\n")
224        parts.append("\n")
225        return parts.joined(separator: "\n")
226    }
227
228    // MARK: - Java
229
230    static let javaSpace = "[ \\t\\n\\r\\f]"
231    static let javaPackage = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*package\(javaSpace)+([\\p{L}\\p{N}_.]+)\(javaSpace)*;$")
232    static let javaImports = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*import(?:\(javaSpace)+static)?\(javaSpace)+([\\p{L}\\p{N}_.*]+)\(javaSpace)*;$")
233    static let javaClass = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*(?:public\(javaSpace)+)?class\(javaSpace)+([\\p{L}\\p{N}_]+)\(javaSpace)*\\{")
234    static let javaMain = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*public\(javaSpace)+static\(javaSpace)+void\(javaSpace)+main\(javaSpace)*\\(\(javaSpace)*String"
235        + "[\\p{L}\\p{N}_\\[\\] \\t\\n\\r\\f]+\\)\(javaSpace)*(?:throws[\\p{L}\\p{N}_,. \\t\\n\\r\\f]+)?\\{")
236    static let javaMethod = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*(?:[\\p{L}\\p{N}]+\(javaSpace)+)?(?:static\(javaSpace)+)?[\\p{L}\\p{N}_\\[\\]]+\(javaSpace)+[\\p{L}\\p{N}_]+"
237        + "\(javaSpace)*\\([\\p{L}\\p{N}_\\[\\], \\t\\n\\r\\f]*\\)\(javaSpace)*(?:throws[\\p{L}\\p{N}_,. \\t\\n\\r\\f]+)?\\{")
238
239    /// `org-babel-expand-body:java`: the body wrapped in a `main` method when it has no method,
240    /// and in a class when it has none, with `:var` as static fields, `:imports` and the
241    /// package of `:classname`.
242    static func expandJava(_ body: String, vars: [(String, VarValue)], params: Params) throws -> String {
243        let fullName = params.single[":classname"] ?? javaClassName(body)
244        let className = fullName.components(separatedBy: ".").last!
245        let packageName = fullName.contains(".") ? (fullName as NSString).deletingPathExtension : nil
246        let varLines = try vars.map { name, value -> String in
247            // A rule is `hline`, a string.
248            let typed: [Cell]
249            if case .table(let rows) = value, rows.contains(where: { $0 == nil }) {
250                typed = rows.compactMap { $0 }.flatMap { $0 } + [.text("hline")]
251            } else {
252                typed = try cells(value)
253            }
254            guard let type = baseType(typed) else { throw Failure.message("A :var value has no type java can take.") }
255            let base = type == .integer ? "Integer" : type == .float ? "Double" : "String"
256            func literal(_ cell: Cell?) throws -> String {
257                guard let cell else { return "null" }
258                switch type {
259                case .integer: return cellText(cell)
260                case .float: return String(format: "%f", Double(cellText(cell)) ?? 0)
261                case .string:
262                    let s = cellText(cell)
263                    if s.range(of: ".\\n+.", options: .regularExpression) != nil { throw Failure.message("Java does not support multiline string literals") }
264                    return "\"" + s + "\""
265                }
266            }
267            let declared: String
268            let data: String
269            switch value {
270            case .scalar, .number:
271                declared = base
272                data = try literal(try cells(value)[0])
273            case .list(let items):
274                declared = "List<\(base)>"
275                data = "Arrays.asList(" + (try items.map { try literal($0) }).joined(separator: ", ") + ")"
276            case .table(let rows):
277                declared = "List<List<\(base)>>"
278                data = "Arrays.asList(" + (try rows.map { row in
279                    try row.map { "Arrays.asList(" + (try $0.map { try literal($0) }).joined(separator: ", ") + ")" } ?? "null"
280                }).joined(separator: ", ") + ")"
281            }
282            return "    static \(declared) \(name) = \(data);"
283        }
284        let imports = try headerValue(":imports", params).map { value -> [String] in
285            guard case .string(let s) = value else { throw Failure.message(":imports for java takes a string.") }
286            return s.components(separatedBy: " ")
287        }
288
289        let buffer = NSMutableString(string: (params.single[":prologue"].map { $0 + "\n" } ?? "") + body + (params.single[":epilogue"].map { "\n" + $0 } ?? ""))
290        func search(_ regex: NSRegularExpression, from point: Int) -> NSTextCheckingResult? {
291            regex.firstMatch(in: buffer as String, options: [.withTransparentBounds, .withoutAnchoringBounds],
292                             range: NSRange(location: point, length: buffer.length - point))
293        }
294        // `org-babel-java--move-past`.
295        func movePast(_ regex: NSRegularExpression, _ point: inout Int) {
296            while let m = search(regex, from: point) { point = min(NSMaxRange(m.range) + 1, buffer.length) }
297        }
298        func wrap(_ opening: String) {
299            var point = 0
300            movePast(javaPackage, &point)
301            movePast(javaImports, &point)
302            buffer.insert(opening, at: point)
303            indentRigidly(buffer, from: point + opening.utf16.count, by: 4)
304            buffer.append("\n}")
305        }
306        if search(javaMain, from: 0) == nil, search(javaMethod, from: 0) == nil { wrap("public static void main(String[] args) {\n") }
307        if search(javaClass, from: 0) == nil { wrap("\npublic class \(className) {\n") }
308        if !varLines.isEmpty {
309            var point = 0
310            movePast(javaClass, &point)
311            buffer.insert(varLines.joined(separator: "\n") + "\n", at: point)
312        }
313        if let imports {
314            var point = 0
315            movePast(javaPackage, &point)
316            buffer.insert(imports.map { "import \($0);" }.joined(separator: "\n") + "\n", at: point)
317        }
318        if let packageName, search(javaPackage, from: 0) == nil { buffer.insert("package \(packageName);\n", at: 0) }
319        return buffer as String
320    }
321
322    /// `org-babel-java-find-classname`.
323    static func javaClassName(_ body: String) -> String {
324        let ns = body as NSString
325        func first(_ regex: NSRegularExpression) -> String? {
326            regex.firstMatch(in: body, range: NSRange(location: 0, length: ns.length)).map { ns.substring(with: $0.range(at: 1)) }
327        }
328        let package = first(javaPackage)
329        let className = first(javaClass)
330        if let package, let className { return package + "." + className }
331        return className ?? package.map { $0 + ".Main" } ?? "Main"
332    }
333
334    /// `indent-code-rigidly` from `start`, at the beginning of a line, to the end: each line
335    /// not starting inside a string moved right, blank lines emptied, with `indent-tabs-mode` nil.
336    static func indentRigidly(_ buffer: NSMutableString, from start: Int, by columns: Int) {
337        var location = start
338        var inString = false
339        while location < buffer.length {
340            if !inString {
341                var end = location
342                var column = 0
343                while end < buffer.length, let c = UnicodeScalar(buffer.character(at: end)), c == " " || c == "\t" {
344                    column = c == "\t" ? (column / 8 + 1) * 8 : column + 1
345                    end += 1
346                }
347                let atEnd = end >= buffer.length || buffer.character(at: end) == 10
348                let indent = atEnd ? "" : String(repeating: " ", count: column + columns)
349                buffer.replaceCharacters(in: NSRange(location: location, length: end - location), with: indent)
350            }
351            let line = buffer.lineRange(for: NSRange(location: location, length: 0))
352            var escaped = false
353            for i in line.location..<NSMaxRange(line) {
354                let c = buffer.character(at: i)
355                if escaped { escaped = false } else if c == 92 { escaped = true } else if c == 34 { inString.toggle() }
356            }
357            location = NSMaxRange(line)
358        }
359    }
360
361    // MARK: - Fortran
362
363    /// `org-babel-expand-body:fortran`.
364    static func expandFortran(_ body: String, vars: [(String, VarValue)], params: Params, properties: (String) -> String? = { _ in nil }) throws -> String {
365        let prologue = params.single[":prologue"]
366        let epilogue = params.single[":epilogue"]
367        // `%S`.
368        func printed(_ cell: Cell) -> String {
369            switch cell {
370            case .number(let n): n
371            case .text(let s): lispString(s)
372            }
373        }
374        // `org-babel-fortran-var-to-fortran`.
375        let declarations = try vars.map { name, value -> String in
376            switch value {
377            case .number(let n) where baseType([.number(n)]) == .integer: return "integer, parameter  ::  \(name) = \(n)\n"
378            case .number(let n): return "real, parameter ::  \(name) = \(n)\n"
379            case .scalar(let s): return "character(len=\(s.unicodeScalars.count)), parameter ::  \(name) = '\(s)'\n"
380            case .table(let rows):
381                guard !rows.contains(where: { $0 == nil }) else { throw Failure.message("Tables with rules in :var aren't supported for fortran yet.") }
382                let rows = rows.map { $0! }
383                let columns = rows.first?.count ?? 0
384                let data = "(/" + rows.map { "(/" + $0.map(printed).joined(separator: ", ") + "/)" }.joined(separator: ", ") + "/)"
385                return "real, parameter :: \(name)(\(rows.count),\(columns)) = transpose( reshape( \(data) , (/ \(columns), \(rows.count) /) ) )\n"
386            case .list(let items):
387                return "real, parameter :: \(name)(\(items.count)) = (/" + items.map(printed).joined(separator: ", ") + "/)\n"
388            }
389        }
390        func items(_ value: HeaderValue?) -> [String] {
391            switch value {
392            case .string(let s): [s]
393            case .items(let items): items
394            case nil: []
395            }
396        }
397        let includes = items(try headerValue(":includes", params) ?? propertyValue(properties("includes")))
398        let defines = items(try headerValue(":defines", params) ?? propertyValue(properties("defines")))
399        let main: String
400        if params.single[":main"] == "no" {
401            main = (prologue.map { $0 + "\n" } ?? "") + body + (epilogue.map { "\n" + $0 + "\n" } ?? "")
402        } else {
403            // Org writes the prologue again where the epilogue would go.
404            let inner = declarations.joined(separator: "\n") + (prologue.map { $0 + "\n" } ?? "") + body + (prologue.map { $0 + "\n" } ?? "")
405            if inner.range(of: "(?im)^[ \\t]*program\\b", options: .regularExpression) != nil {
406                if !vars.isEmpty { throw Failure.message("Cannot use :vars if `program' statement is present") }
407                main = inner
408            } else {
409                main = "program main\n\(inner)\nend program main\n"
410            }
411        }
412        return [includes.map { "#include \($0)" }.joined(separator: "\n"), defines.map { "#define \($0)" }.joined(separator: "\n"), main, "\n"]
413            .joined(separator: "\n")
414    }
415
416    // MARK: - Clojure
417
418    /// `org-babel-expand-body:clojure`, for a backend other than CIDER's (whose namespace
419    /// only matters with `:ns`, which is then used).
420    static func expandClojure(_ body: String, vars: [(String, VarValue)], params: Params) -> String {
421        var expanded = params.single[":ns"].map { "(ns \($0))\n" } ?? ""
422        if vars.isEmpty {
423            expanded += body.trimmingCharacters(in: .whitespacesAndNewlines)
424        } else {
425            let uncommented = body.replacingOccurrences(of: "(?m)^[ ]*;+.*$", with: "", options: .regularExpression)
426            expanded += "(let [" + vars.map { "\($0.0) '\(lispLiteral($0.1))" }.joined(separator: "\n      ") + "]\n\(uncommented))"
427        }
428        expanded = expanded.trimmingCharacters(in: .whitespacesAndNewlines)
429        if params.results.contains("output") { return expanded }
430        let printer = params.results.contains("code") || params.results.contains("pp")
431            ? "(require '[clojure.pprint :refer [pprint]]) (pprint "
432            : "(prn "
433        return printer + "(binding [*out* (java.io.StringWriter.)]" + expanded + "))"
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    }
640}