krz/orgstar

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

Sources/OrgCore/Compute/BabelExpand.swift

401 lines · 20963 bytes

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