Commit dc34b95792
Verified · cmc
Layout: unified · split
Sources/OrgCore/Agenda/Agenda.swift +38 −7
| @@ -8,6 +8,7 @@ public struct AgendaItem: Sendable, Equatable { | |||
| 8 | case pastScheduled = "past-scheduled" | 8 | case pastScheduled = "past-scheduled" |
| 9 | case timestamp | 9 | case timestamp |
| 10 | case block | 10 | case block |
| 11 | case sexp | ||
| 11 | case timeGrid | 12 | case timeGrid |
| 12 | case currentTime | 13 | case currentTime |
| 13 | case todo | 14 | case todo |
| @@ -108,7 +109,16 @@ public enum Agenda { | |||
| 108 | static func entries(_ source: AgendaSource, day: Int, today: Int, options: AgendaOptions) -> [AgendaItem] { | 109 | static func entries(_ source: AgendaSource, day: Int, today: Int, options: AgendaOptions) -> [AgendaItem] { |
| 109 | let deadlines = self.deadlines(source, day: day, today: today, options: options) | 110 | let deadlines = self.deadlines(source, day: day, today: today, options: options) |
| 110 | return (options.logMode ? progress(source, day: day, options: options) : []) + deadlines + scheduled(source, day: day, today: today, options: options) + blocks(source, day: day, options: options) | 111 | return (options.logMode ? progress(source, day: day, options: options) : []) + deadlines + scheduled(source, day: day, today: today, options: options) + blocks(source, day: day, options: options) |
| 111 | + timestamps(source, day: day, today: today, options: options) | 112 | + timestamps(source, day: day, today: today, options: options) + sexps(source, day: day, options: options) |
| 113 | } | ||
| 114 | |||
| 115 | /// `org-time-string-to-absolute` for a planning timestamp's text: a diary sexp counts on | ||
| 116 | /// `current` when it applies then, and otherwise the entry is skipped. | ||
| 117 | static func planningDay(_ s: String, current: Int) -> (day: Int, sexp: Bool)? { | ||
| 118 | guard s.hasPrefix("%%(") else { return Days.absolute(of: s).map { ($0, false) } } | ||
| 119 | guard let close = s.lastIndex(of: ")"), let (sexp, _) = try? LispReader.readFirst(String(s[s.index(s.startIndex, offsetBy: 2)...close])), | ||
| 120 | DiarySexp.matches(sexp, day: current) else { return nil } | ||
| 121 | return (current, true) | ||
| 112 | } | 122 | } |
| 113 | 123 | ||
| 114 | // MARK: - Collectors | 124 | // MARK: - Collectors |
| @@ -117,8 +127,8 @@ public enum Agenda { | |||
| 117 | let isToday = current == today | 127 | let isToday = current == today |
| 118 | var items: [AgendaItem] = [] | 128 | var items: [AgendaItem] = [] |
| 119 | for heading in source.headings { | 129 | for heading in source.headings { |
| 120 | guard !heading.skipped, let (s, offset) = heading.deadline, let deadline = Days.absolute(of: s) else { continue } | 130 | guard !heading.skipped, let (s, offset) = heading.deadline, let (deadline, sexp) = planningDay(s, current: current) else { continue } |
| 121 | let repeat_ = current <= today ? deadline : (Days.closest(s, to: current, prefer: .future) ?? deadline) | 131 | let repeat_ = sexp || current <= today ? deadline : (Days.closest(s, to: current, prefer: .future) ?? deadline) |
| 122 | let diff = deadline - current | 132 | let diff = deadline - current |
| 123 | let warningDays = Days.warningDays(s, delay: false, defaultDays: options.deadlineWarningDays) | 133 | let warningDays = Days.warningDays(s, delay: false, defaultDays: options.deadlineWarningDays) |
| 124 | if current != deadline, current != repeat_ { | 134 | if current != deadline, current != repeat_ { |
| @@ -152,8 +162,8 @@ public enum Agenda { | |||
| 152 | let isToday = current == today | 162 | let isToday = current == today |
| 153 | var items: [AgendaItem] = [] | 163 | var items: [AgendaItem] = [] |
| 154 | for heading in source.headings { | 164 | for heading in source.headings { |
| 155 | guard !heading.skipped, let (s, offset) = heading.scheduled, let schedule = Days.absolute(of: s) else { continue } | 165 | guard !heading.skipped, let (s, offset) = heading.scheduled, let (schedule, sexp) = planningDay(s, current: current) else { continue } |
| 156 | let repeat_ = current <= today ? schedule : (Days.closest(s, to: current, prefer: .future) ?? schedule) | 166 | let repeat_ = sexp || current <= today ? schedule : (Days.closest(s, to: current, prefer: .future) ?? schedule) |
| 157 | let diff = current - schedule | 167 | let diff = current - schedule |
| 158 | let past = schedule < today | 168 | let past = schedule < today |
| 159 | let delay = Days.warningDays(s, delay: true, defaultDays: 0) | 169 | let delay = Days.warningDays(s, delay: true, defaultDays: 0) |
| @@ -236,6 +246,7 @@ public enum Agenda { | |||
| 236 | return items | 246 | return items |
| 237 | } | 247 | } |
| 238 | 248 | ||
| 249 | static let diaryStamp = try! NSRegularExpression(pattern: "^<%%(\\([^>\\n]+\\))([^\\n>]*)>") | ||
| 239 | static let repeating = try! NSRegularExpression(pattern: "^<[0-9]+-[0-9]+-[0-9]+[^>\\n]+?\\+[0-9]+[hdwmy]>") | 250 | static let repeating = try! NSRegularExpression(pattern: "^<[0-9]+-[0-9]+-[0-9]+[^>\\n]+?\\+[0-9]+[hdwmy]>") |
| 240 | static let stampBoth = try! NSRegularExpression(pattern: "^[\\[<](\(AgendaSource.tsInternal))[\\]>]") | 251 | static let stampBoth = try! NSRegularExpression(pattern: "^[\\[<](\(AgendaSource.tsInternal))[\\]>]") |
| 241 | static let stampActive = try! NSRegularExpression(pattern: "<(\(AgendaSource.tsInternal))>") | 252 | static let stampActive = try! NSRegularExpression(pattern: "<(\(AgendaSource.tsInternal))>") |
| @@ -252,6 +263,9 @@ public enum Agenda { | |||
| 252 | if stamp.rest.hasPrefix(literal) { | 263 | if stamp.rest.hasPrefix(literal) { |
| 253 | guard let m = stampBoth.firstMatch(in: stamp.rest, range: all) else { continue } | 264 | guard let m = stampBoth.firstMatch(in: stamp.rest, range: all) else { continue } |
| 254 | timestamp = rest.substring(with: m.range) | 265 | timestamp = rest.substring(with: m.range) |
| 266 | } else if let m = diaryStamp.firstMatch(in: stamp.rest, range: all) { | ||
| 267 | guard let (sexp, _) = try? LispReader.readFirst(rest.substring(with: m.range(at: 1))), DiarySexp.matches(sexp, day: current) else { continue } | ||
| 268 | timestamp = rest.substring(with: m.range) | ||
| 255 | } else if let m = repeating.firstMatch(in: stamp.rest, range: all) { | 269 | } else if let m = repeating.firstMatch(in: stamp.rest, range: all) { |
| 256 | let repeat_ = rest.substring(with: m.range) | 270 | let repeat_ = rest.substring(with: m.range) |
| 257 | guard let past = Days.closest(repeat_, to: current > today ? today : current, prefer: .past) else { continue } | 271 | guard let past = Days.closest(repeat_, to: current > today ? today : current, prefer: .past) else { continue } |
| @@ -271,6 +285,23 @@ public enum Agenda { | |||
| 271 | return items | 285 | return items |
| 272 | } | 286 | } |
| 273 | 287 | ||
| 288 | /// `org-agenda-get-sexps`. | ||
| 289 | static func sexps(_ source: AgendaSource, day current: Int, options: AgendaOptions) -> [AgendaItem] { | ||
| 290 | var items: [AgendaItem] = [] | ||
| 291 | for line in source.sexps { | ||
| 292 | guard let results = DiarySexp.entries(line.sexp, entry: line.entry, day: current) else { continue } | ||
| 293 | let heading = source.headings[line.heading] | ||
| 294 | for result in results { | ||
| 295 | let txt = result.contains(where: { !$0.isWhitespace }) ? result : "SEXP entry returned empty string" | ||
| 296 | items.append(format( | ||
| 297 | source, heading, kind: .sexp, marker: line.offset, extra: "", dotime: .headline, removing: nil, | ||
| 298 | trailing: options.gridTrailing, prefix: options.prefix("agenda"), head: txt, attached: false, urgency: { _ in 0 } | ||
| 299 | )) | ||
| 300 | } | ||
| 301 | } | ||
| 302 | return items | ||
| 303 | } | ||
| 304 | |||
| 274 | /// `" HH:MM"` in a planning timestamp: the time and everything after it. | 305 | /// `" HH:MM"` in a planning timestamp: the time and everything after it. |
| 275 | static func timeIn(_ s: String) -> String? { | 306 | static func timeIn(_ s: String) -> String? { |
| 276 | guard let r = s.range(of: " [012]?[0-9]:[0-9][0-9]", options: .regularExpression) else { return nil } | 307 | guard let r = s.range(of: " [012]?[0-9]:[0-9][0-9]", options: .regularExpression) else { return nil } |
| @@ -303,7 +334,7 @@ public enum Agenda { | |||
| 303 | static func format( | 334 | static func format( |
| 304 | _ source: AgendaSource, _ heading: AgendaSource.Heading, kind: AgendaItem.Kind, marker: Int, | 335 | _ source: AgendaSource, _ heading: AgendaSource.Heading, kind: AgendaItem.Kind, marker: Int, |
| 305 | extra: String, dotime: Dotime?, removing: NSRegularExpression?, trailing: String, prefix prefixFormat: PrefixFormat, | 336 | extra: String, dotime: Dotime?, removing: NSRegularExpression?, trailing: String, prefix prefixFormat: PrefixFormat, |
| 306 | head: String? = nil, habit: Habit? = nil, urgency: (Int) -> Int | 337 | head: String? = nil, habit: Habit? = nil, attached: Bool = true, urgency: (Int) -> Int |
| 307 | ) -> AgendaItem { | 338 | ) -> AgendaItem { |
| 308 | var txt = (head ?? heading.head).trimmingCharacters(in: .whitespaces) | 339 | var txt = (head ?? heading.head).trimmingCharacters(in: .whitespaces) |
| 309 | // `org-agenda-fix-displayed-tags`: the heading's tags are replaced by the full list. | 340 | // `org-agenda-fix-displayed-tags`: the heading's tags are replaced by the full list. |
| @@ -340,7 +371,7 @@ public enum Agenda { | |||
| 340 | txt = highlightTodo(txt, keywords: source.keywords) | 371 | txt = highlightTodo(txt, keywords: source.keywords) |
| 341 | let priority = Self.priority(prefix + txt, source.priorities) | 372 | let priority = Self.priority(prefix + txt, source.priorities) |
| 342 | return AgendaItem( | 373 | return AgendaItem( |
| 343 | kind: kind, path: source.path, headingOffset: heading.start, headingLine: heading.line, markerOffset: marker, category: category, | 374 | kind: kind, path: source.path, headingOffset: attached ? heading.start : nil, headingLine: attached ? heading.line : nil, markerOffset: marker, category: category, |
| 344 | timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name), | 375 | timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name), |
| 345 | todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority), | 376 | todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority), |
| 346 | warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit, | 377 | warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit, |
Sources/OrgCore/Agenda/AgendaSource.swift +29 −1
| @@ -74,8 +74,20 @@ public struct AgendaSource: Sendable { | |||
| 74 | let note: String? | 74 | let note: String? |
| 75 | } | 75 | } |
| 76 | 76 | ||
| 77 | /// A `%%(…)` line (`org-agenda-get-sexps`). | ||
| 78 | struct SexpLine: Sendable { | ||
| 79 | /// Start of the line. | ||
| 80 | let offset: Int | ||
| 81 | /// The heading the line is under. | ||
| 82 | let heading: Int | ||
| 83 | let sexp: Sexp | ||
| 84 | /// The rest of the line after the sexp. | ||
| 85 | let entry: String | ||
| 86 | } | ||
| 87 | |||
| 77 | let headings: [Heading] | 88 | let headings: [Heading] |
| 78 | let stamps: [Stamp] | 89 | let stamps: [Stamp] |
| 90 | let sexps: [SexpLine] | ||
| 79 | let blocks: [Block] | 91 | let blocks: [Block] |
| 80 | let progress: [Progress] | 92 | let progress: [Progress] |
| 81 | /// The file's TODO keywords, active and done. | 93 | /// The file's TODO keywords, active and done. |
| @@ -244,12 +256,28 @@ public struct AgendaSource: Sendable { | |||
| 244 | for offset in stampOffsets.sorted() { | 256 | for offset in stampOffsets.sorted() { |
| 245 | guard let owner = heading(at: offset), !skipped[owner] else { continue } | 257 | guard let owner = heading(at: offset), !skipped[owner] else { continue } |
| 246 | let rest = restOfLine(offset) | 258 | let rest = restOfLine(offset) |
| 247 | guard rest.range(of: "^<[0-9]{4}-[0-9]{2}-[0-9]{2}", options: .regularExpression) != nil else { continue } | 259 | guard rest.range(of: "^<([0-9]{4}-[0-9]{2}-[0-9]{2}|%%\\()", options: .regularExpression) != nil else { continue } |
| 248 | if Self.inDateRange(offset, ns) { continue } | 260 | if Self.inDateRange(offset, ns) { continue } |
| 249 | stamps.append(Stamp(offset: offset, heading: owner, rest: rest)) | 261 | stamps.append(Stamp(offset: offset, heading: owner, rest: rest)) |
| 250 | } | 262 | } |
| 251 | self.stamps = stamps | 263 | self.stamps = stamps |
| 252 | 264 | ||
| 265 | // Sexp lines: org searches the raw text for `%%(` at the start of a line. Before the | ||
| 266 | // first heading org's agenda signals an error; they're left out. | ||
| 267 | var sexps: [SexpLine] = [] | ||
| 268 | let sexpLine = try! NSRegularExpression(pattern: "^&?%%\\(", options: .anchorsMatchLines) | ||
| 269 | for m in sexpLine.matches(in: text, range: NSRange(location: 0, length: ns.length)) { | ||
| 270 | guard let owner = heading(at: m.range.location), !skipped[owner] else { continue } | ||
| 271 | let open = NSMaxRange(m.range) - 1 | ||
| 272 | let tail = ns.substring(with: NSRange(location: open, length: min(ns.length - open, 4096))) | ||
| 273 | guard let (sexp, length) = try? LispReader.readFirst(tail) else { continue } | ||
| 274 | let afterSexp = String(tail.dropFirst(length)) | ||
| 275 | let lineRest = afterSexp.prefix { $0 != "\n" && $0 != "\r" } | ||
| 276 | let entry = lineRest.trimmingCharacters(in: CharacterSet(charactersIn: " \t")) | ||
| 277 | sexps.append(SexpLine(offset: m.range.location, heading: owner, sexp: sexp, entry: entry)) | ||
| 278 | } | ||
| 279 | self.sexps = sexps | ||
| 280 | |||
| 253 | // Ranges: org scans the raw text, skipping comment lines, skipped trees and src blocks. | 281 | // Ranges: org scans the raw text, skipping comment lines, skipped trees and src blocks. |
| 254 | var blocks: [Block] = [] | 282 | var blocks: [Block] = [] |
| 255 | let range = try! NSRegularExpression(pattern: "<(\(Self.tsInternal))>--?-?<(\(Self.tsInternal))>") | 283 | let range = try! NSRegularExpression(pattern: "<(\(Self.tsInternal))>--?-?<(\(Self.tsInternal))>") |
Sources/OrgCore/Agenda/DiarySexp.swift added +273
| @@ -0,0 +1,273 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | /// Diary sexps, `%%(…)` lines and `<%%(…)>` timestamps: `org-diary-sexp-entry` with the | ||
| 4 | /// calendar, diary-lib and org functions agenda files use. A sexp that uses anything else, or | ||
| 5 | /// signals an error, doesn't apply, as Emacs reports a bad sexp and moves on. | ||
| 6 | public enum DiarySexp { | ||
| 7 | /// The entries `sexp` gives on `day` for a diary line whose text is `entry`, or nil where | ||
| 8 | /// it doesn't apply. | ||
| 9 | public static func entries(_ sexp: Sexp, entry: String, day: Int) -> [String]? { | ||
| 10 | let lisp = interpreter(day: day, entry: entry) | ||
| 11 | guard let result = try? lisp.eval(sexp), !Elisp.isNil(result) else { return nil } | ||
| 12 | switch result { | ||
| 13 | case .string(let s): return s.components(separatedBy: "; ") | ||
| 14 | case .dotted(let items, .string(let s)) where items.count == 1: return [s] | ||
| 15 | case .list(let items) where items.first?.string != nil: return items.map { Elisp.printed($0) } | ||
| 16 | default: return [entry] | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | /// Whether `sexp` applies on `day`, for diary timestamps. | ||
| 21 | public static func matches(_ sexp: Sexp, day: Int) -> Bool { | ||
| 22 | entries(sexp, entry: "", day: day) != nil | ||
| 23 | } | ||
| 24 | |||
| 25 | static func interpreter(day: Int, entry: String) -> Elisp { | ||
| 26 | let lisp = Elisp() | ||
| 27 | lisp.set("date", gregorian(day)) | ||
| 28 | lisp.set("entry", .string(entry)) | ||
| 29 | lisp.set("calendar-date-style", .symbol("american")) | ||
| 30 | define(lisp) | ||
| 31 | return lisp | ||
| 32 | } | ||
| 33 | |||
| 34 | /// `(MONTH DAY YEAR)`. | ||
| 35 | static func gregorian(_ day: Int) -> Sexp { | ||
| 36 | let date = Days.date(day) | ||
| 37 | return .list([.integer(date.month), .integer(date.day), .integer(date.year)]) | ||
| 38 | } | ||
| 39 | |||
| 40 | static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) { | ||
| 41 | let items = try Elisp.elements(date) | ||
| 42 | guard items.count == 3 else { throw Elisp.Signal(message: "Bad date \(date.description)") } | ||
| 43 | return (try Elisp.int(items[0]), try Elisp.int(items[1]), try Elisp.int(items[2])) | ||
| 44 | } | ||
| 45 | |||
| 46 | static func absolute(_ date: Sexp) throws -> Int { | ||
| 47 | let d = try parts(date) | ||
| 48 | return Days.absolute(year: d.year, month: d.month, day: d.day) | ||
| 49 | } | ||
| 50 | |||
| 51 | static func isLeap(_ year: Int) -> Bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } | ||
| 52 | |||
| 53 | static func lastDay(month: Int, year: Int) -> Int { | ||
| 54 | month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(month - 1 + 12) % 12] | ||
| 55 | } | ||
| 56 | |||
| 57 | /// `calendar-nth-named-absday`. | ||
| 58 | static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) -> Int { | ||
| 59 | func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 } | ||
| 60 | if n > 0 { | ||
| 61 | return 7 * (n - 1) + onOrBefore(6 + Days.absolute(year: year, month: month, day: day ?? 1)) | ||
| 62 | } | ||
| 63 | return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: day ?? lastDay(month: month, year: year))) | ||
| 64 | } | ||
| 65 | |||
| 66 | /// `diary-ordinal-suffix`. | ||
| 67 | static func ordinalSuffix(_ n: Int) throws -> String { | ||
| 68 | if [11, 12, 13].contains(n % 100) || 3 < n % 10 { return "th" } | ||
| 69 | guard n % 10 >= 0 else { throw Elisp.Signal(message: "Args out of range") } | ||
| 70 | return ["th", "st", "nd", "rd"][n % 10] | ||
| 71 | } | ||
| 72 | |||
| 73 | /// `diary-make-date`: arguments in `calendar-date-style` order, as `(MONTH DAY YEAR)`. | ||
| 74 | static func makeDate(_ lisp: Elisp, _ a: Sexp, _ b: Sexp, _ c: Sexp) throws -> [Sexp] { | ||
| 75 | switch try lisp.value("calendar-date-style") { | ||
| 76 | case .symbol("iso"): [b, c, a] | ||
| 77 | case .symbol("european"): [b, a, c] | ||
| 78 | default: [a, b, c] | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | /// Runs `body` with `calendar-date-style` bound to `iso`, as the `org-` wrappers do. | ||
| 83 | static func iso(_ lisp: Elisp, _ body: () throws -> Sexp) rethrows -> Sexp { | ||
| 84 | try lisp.binding(["calendar-date-style": .symbol("iso")], body) | ||
| 85 | } | ||
| 86 | |||
| 87 | static func optional(_ args: [Sexp], _ i: Int) -> Sexp { i < args.count ? args[i] : .nil } | ||
| 88 | |||
| 89 | static func define(_ lisp: Elisp) { | ||
| 90 | func date() throws -> Sexp { try lisp.value("date") } | ||
| 91 | func entry() throws -> Sexp { try lisp.value("entry") } | ||
| 92 | func arity(_ args: [Sexp], _ range: ClosedRange<Int>, _ name: String) throws { | ||
| 93 | try Elisp.arity(args, range, name) | ||
| 94 | } | ||
| 95 | |||
| 96 | lisp.define("calendar-extract-month") { _, args in try arity(args, 1...1, "calendar-extract-month"); return .integer(try parts(args[0]).month) } | ||
| 97 | lisp.define("calendar-extract-day") { _, args in try arity(args, 1...1, "calendar-extract-day"); return .integer(try parts(args[0]).day) } | ||
| 98 | lisp.define("calendar-extract-year") { _, args in try arity(args, 1...1, "calendar-extract-year"); return .integer(try parts(args[0]).year) } | ||
| 99 | lisp.define("calendar-absolute-from-gregorian") { _, args in | ||
| 100 | try arity(args, 1...1, "calendar-absolute-from-gregorian") | ||
| 101 | return .integer(try absolute(args[0])) | ||
| 102 | } | ||
| 103 | lisp.define("calendar-gregorian-from-absolute") { _, args in | ||
| 104 | try arity(args, 1...1, "calendar-gregorian-from-absolute") | ||
| 105 | return gregorian(try Elisp.int(args[0])) | ||
| 106 | } | ||
| 107 | lisp.define("calendar-day-of-week") { _, args in | ||
| 108 | try arity(args, 1...1, "calendar-day-of-week") | ||
| 109 | return .integer(Days.weekday(try absolute(args[0]))) | ||
| 110 | } | ||
| 111 | lisp.define("calendar-leap-year-p") { _, args in | ||
| 112 | try arity(args, 1...1, "calendar-leap-year-p") | ||
| 113 | return Elisp.bool(isLeap(try Elisp.int(args[0]))) | ||
| 114 | } | ||
| 115 | lisp.define("calendar-last-day-of-month") { _, args in | ||
| 116 | try arity(args, 2...2, "calendar-last-day-of-month") | ||
| 117 | return .integer(lastDay(month: try Elisp.int(args[0]), year: try Elisp.int(args[1]))) | ||
| 118 | } | ||
| 119 | lisp.define("calendar-date-equal") { _, args in | ||
| 120 | try arity(args, 2...2, "calendar-date-equal") | ||
| 121 | let a = try parts(args[0]) | ||
| 122 | let b = try parts(args[1]) | ||
| 123 | return Elisp.bool(a == b) | ||
| 124 | } | ||
| 125 | lisp.define("calendar-day-number") { _, args in | ||
| 126 | try arity(args, 1...1, "calendar-day-number") | ||
| 127 | let d = try parts(args[0]) | ||
| 128 | return .integer(Days.absolute(year: d.year, month: d.month, day: d.day) - Days.absolute(year: d.year, month: 1, day: 0)) | ||
| 129 | } | ||
| 130 | lisp.define("calendar-nth-named-absday") { _, args in | ||
| 131 | try arity(args, 4...5, "calendar-nth-named-absday") | ||
| 132 | let day = Elisp.isNil(optional(args, 4)) ? nil : try Elisp.int(args[4]) | ||
| 133 | return .integer(nthNamedAbsday(try Elisp.int(args[0]), try Elisp.int(args[1]), try Elisp.int(args[2]), try Elisp.int(args[3]), day)) | ||
| 134 | } | ||
| 135 | lisp.define("calendar-nth-named-day") { lisp, args in | ||
| 136 | gregorian(try Elisp.int(try lisp.callNamed("calendar-nth-named-absday", args))) | ||
| 137 | } | ||
| 138 | lisp.define("calendar-iso-from-absolute") { _, args in | ||
| 139 | try arity(args, 1...1, "calendar-iso-from-absolute") | ||
| 140 | let day = try Elisp.int(args[0]) | ||
| 141 | let thursday = day - (Days.weekday(day) + 6) % 7 + 3 | ||
| 142 | return .list([.integer(Days.isoWeek(day)), .integer(Days.weekday(day)), .integer(Days.date(thursday).year)]) | ||
| 143 | } | ||
| 144 | |||
| 145 | lisp.define("diary-ordinal-suffix") { _, args in | ||
| 146 | try arity(args, 1...1, "diary-ordinal-suffix") | ||
| 147 | return .string(try ordinalSuffix(try Elisp.int(args[0]))) | ||
| 148 | } | ||
| 149 | lisp.define("diary-make-date") { lisp, args in | ||
| 150 | try arity(args, 3...3, "diary-make-date") | ||
| 151 | return .list(try makeDate(lisp, args[0], args[1], args[2])) | ||
| 152 | } | ||
| 153 | lisp.define("diary-date") { lisp, args in | ||
| 154 | try arity(args, 3...4, "diary-date") | ||
| 155 | let wanted = try makeDate(lisp, args[0], args[1], args[2]) | ||
| 156 | let d = try parts(try date()) | ||
| 157 | for (spec, actual) in zip(wanted, [d.month, d.day, d.year]) { | ||
| 158 | let holds = switch spec { | ||
| 159 | case .symbol("t"): true | ||
| 160 | case .list(let items): items.contains(.integer(actual)) | ||
| 161 | case .integer(let i): i == actual | ||
| 162 | default: false | ||
| 163 | } | ||
| 164 | if !holds { return .nil } | ||
| 165 | } | ||
| 166 | return Elisp.cons(optional(args, 3), try entry()) | ||
| 167 | } | ||
| 168 | lisp.define("diary-block") { lisp, args in | ||
| 169 | try arity(args, 6...7, "diary-block") | ||
| 170 | let first = try absolute(.list(try makeDate(lisp, args[0], args[1], args[2]))) | ||
| 171 | let last = try absolute(.list(try makeDate(lisp, args[3], args[4], args[5]))) | ||
| 172 | let d = try absolute(try date()) | ||
| 173 | return first <= d && d <= last ? Elisp.cons(optional(args, 6), try entry()) : .nil | ||
| 174 | } | ||
| 175 | lisp.define("diary-float") { _, args in | ||
| 176 | try arity(args, 3...5, "diary-float") | ||
| 177 | let month = args[0] | ||
| 178 | let dayname = try Elisp.int(args[1]) | ||
| 179 | let n = try Elisp.int(args[2]) | ||
| 180 | let day = Elisp.isNil(optional(args, 3)) ? nil : try Elisp.int(args[3]) | ||
| 181 | let current = try absolute(try date()) | ||
| 182 | guard dayname == Days.weekday(current) else { return .nil } | ||
| 183 | let d = Days.date(current) | ||
| 184 | let limit = nthNamedAbsday(-n, dayname, d.month, d.year, d.day) | ||
| 185 | let lastAbs = n > 0 ? limit : limit + 6 | ||
| 186 | let firstAbs = n > 0 ? limit - 6 : limit | ||
| 187 | let first = Days.date(firstAbs) | ||
| 188 | let last = Days.date(lastAbs) | ||
| 189 | func monthMatches(_ m: Int) -> Bool { | ||
| 190 | switch month { | ||
| 191 | case .symbol("t"): true | ||
| 192 | case .list(let items): items.contains(.integer(m)) | ||
| 193 | case .integer(let i): i == m | ||
| 194 | default: false | ||
| 195 | } | ||
| 196 | } | ||
| 197 | func base(_ m: Int, _ y: Int) -> Int { day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) } | ||
| 198 | let applies: Bool | ||
| 199 | if first.month == last.month { | ||
| 200 | let b = base(first.month, first.year) | ||
| 201 | applies = monthMatches(first.month) && first.day <= b && b <= last.day | ||
| 202 | } else { | ||
| 203 | applies = (first.year < last.year || (first.year == last.year && first.month < last.month)) | ||
| 204 | && ((monthMatches(first.month) && first.day <= base(first.month, first.year)) | ||
| 205 | || (monthMatches(last.month) && base(last.month, last.year) <= last.day)) | ||
| 206 | } | ||
| 207 | return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil | ||
| 208 | } | ||
| 209 | lisp.define("diary-anniversary") { lisp, args in | ||
| 210 | try arity(args, 2...4, "diary-anniversary") | ||
| 211 | let made = try makeDate(lisp, args[0], args[1], optional(args, 2)) | ||
| 212 | var dd = try Elisp.int(made[1]) | ||
| 213 | var mm = try Elisp.int(made[0]) | ||
| 214 | let y = try parts(try date()).year | ||
| 215 | let diff = Elisp.isNil(made[2]) ? 100 : y - (try Elisp.int(made[2])) | ||
| 216 | if mm == 2, dd == 29, !isLeap(y) { | ||
| 217 | mm = 3 | ||
| 218 | dd = 1 | ||
| 219 | } | ||
| 220 | let d = try parts(try date()) | ||
| 221 | guard diff > 0, (mm, dd, y) == (d.month, d.day, d.year) else { return .nil } | ||
| 222 | return Elisp.cons(optional(args, 3), .string(try Elisp.format([try entry(), .integer(diff), .string(try ordinalSuffix(diff))]))) | ||
| 223 | } | ||
| 224 | lisp.define("diary-cyclic") { lisp, args in | ||
| 225 | try arity(args, 4...5, "diary-cyclic") | ||
| 226 | let n = try Elisp.int(args[0]) | ||
| 227 | guard n > 0 else { throw Elisp.Signal(message: "Day count must be positive") } | ||
| 228 | let diff = try absolute(try date()) - (try absolute(.list(try makeDate(lisp, args[1], args[2], args[3])))) | ||
| 229 | let cycle = diff / n | ||
| 230 | guard diff >= 0, diff % n == 0 else { return .nil } | ||
| 231 | return Elisp.cons(optional(args, 4), .string(try Elisp.format([try entry(), .integer(cycle), .string(try ordinalSuffix(cycle))]))) | ||
| 232 | } | ||
| 233 | lisp.define("diary-remind") { lisp, args in | ||
| 234 | try arity(args, 2...3, "diary-remind") | ||
| 235 | var days = args[1] | ||
| 236 | if case .integer(let n) = days, n < 0 { days = Elisp.list((1...(-n)).map { .integer($0) }) } | ||
| 237 | func remind(_ days: Sexp) throws -> Sexp { | ||
| 238 | let applies = try lisp.eval(args[0]) | ||
| 239 | if !Elisp.isNil(applies) { return applies } | ||
| 240 | if case .integer(let n) = days { | ||
| 241 | let shifted = gregorian(try absolute(try date()) + n) | ||
| 242 | var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) } | ||
| 243 | guard !Elisp.isNil(found) else { return .nil } | ||
| 244 | if case .dotted = found { found = try Elisp.cdr(found) } else if case .list = found { found = try Elisp.cdr(found) } | ||
| 245 | let span = n % 7 == 0 ? String(format: "%d week%@", n / 7, n == 7 ? "" : "s") : String(format: "%d day%@", n, n == 1 ? "" : "s") | ||
| 246 | return .string("Reminder: Only " + span + " until " + Elisp.printed(found)) | ||
| 247 | } | ||
| 248 | guard case .list(let items) = days, let first = items.first else { return .nil } | ||
| 249 | let head = try remind(first) | ||
| 250 | return Elisp.isNil(head) ? try remind(Elisp.list(Array(items.dropFirst()))) : head | ||
| 251 | } | ||
| 252 | return try remind(days) | ||
| 253 | } | ||
| 254 | |||
| 255 | lisp.define("org-anniversary") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-anniversary", args) } } | ||
| 256 | lisp.define("org-cyclic") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-cyclic", args) } } | ||
| 257 | lisp.define("org-block") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-block", args) } } | ||
| 258 | lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } } | ||
| 259 | lisp.define("org-class") { _, args in | ||
| 260 | try arity(args, 7...Int.max, "org-class") | ||
| 261 | let ints = try args.prefix(7).map(Elisp.int) | ||
| 262 | let first = Days.absolute(year: ints[0], month: ints[1], day: ints[2]) | ||
| 263 | let last = Days.absolute(year: ints[3], month: ints[4], day: ints[5]) | ||
| 264 | let d = try absolute(try date()) | ||
| 265 | let skip = Array(args.dropFirst(7)) | ||
| 266 | // Holiday names and `holidays' need the calendar's holiday lists. | ||
| 267 | guard skip.allSatisfy({ $0.integer != nil }) else { throw Elisp.Unsupported(what: "org-class holidays") } | ||
| 268 | let week = Days.isoWeek(d) | ||
| 269 | guard first <= d, d <= last, Days.weekday(d) == ints[6], !skip.contains(.integer(week)) else { return .nil } | ||
| 270 | return try entry() | ||
| 271 | } | ||
| 272 | } | ||
| 273 | } | ||
Sources/OrgCore/Config/Elisp.swift added +878
| @@ -0,0 +1,878 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | /// A small Emacs Lisp evaluator over `Sexp` values: the special forms and functions diary | ||
| 4 | /// sexps and table formulas use, with dynamic binding. A function or form it doesn't have | ||
| 5 | /// throws `Unsupported`; errors Emacs would signal throw `Signal`. | ||
| 6 | public final class Elisp { | ||
| 7 | public struct Unsupported: Error, Equatable, CustomStringConvertible { | ||
| 8 | public let what: String | ||
| 9 | public var description: String { what } | ||
| 10 | } | ||
| 11 | |||
| 12 | public struct Signal: Error, Equatable, CustomStringConvertible { | ||
| 13 | public let message: String | ||
| 14 | public var description: String { message } | ||
| 15 | } | ||
| 16 | |||
| 17 | public typealias Builtin = (Elisp, [Sexp]) throws -> Sexp | ||
| 18 | |||
| 19 | private var scopes: [[String: Sexp]] = [[:]] | ||
| 20 | private var functions: [String: Builtin] = [:] | ||
| 21 | private var steps = 0 | ||
| 22 | static let stepLimit = 1_000_000 | ||
| 23 | |||
| 24 | public init() { | ||
| 25 | Self.core(self) | ||
| 26 | } | ||
| 27 | |||
| 28 | /// Defines or replaces a function. | ||
| 29 | public func define(_ name: String, _ function: @escaping Builtin) { functions[name] = function } | ||
| 30 | |||
| 31 | /// Sets a global variable. | ||
| 32 | public func set(_ name: String, _ value: Sexp) { scopes[0][name] = value } | ||
| 33 | |||
| 34 | public func value(_ name: String) throws -> Sexp { | ||
| 35 | for scope in scopes.reversed() { if let v = scope[name] { return v } } | ||
| 36 | throw Signal(message: "Symbol’s value as variable is void: \(name)") | ||
| 37 | } | ||
| 38 | |||
| 39 | /// Evaluates `body` with `bindings` added. | ||
| 40 | public func binding<T>(_ bindings: [String: Sexp], _ body: () throws -> T) rethrows -> T { | ||
| 41 | scopes.append(bindings) | ||
| 42 | defer { scopes.removeLast() } | ||
| 43 | return try body() | ||
| 44 | } | ||
| 45 | |||
| 46 | private func assign(_ name: String, _ v: Sexp) { | ||
| 47 | for i in scopes.indices.reversed() where scopes[i][name] != nil { | ||
| 48 | scopes[i][name] = v | ||
| 49 | return | ||
| 50 | } | ||
| 51 | scopes[0][name] = v | ||
| 52 | } | ||
| 53 | |||
| 54 | // MARK: - Values | ||
| 55 | |||
| 56 | public static let t = Sexp.symbol("t") | ||
| 57 | |||
| 58 | public static func isNil(_ v: Sexp) -> Bool { v == .nil || v == .list([]) } | ||
| 59 | public static func bool(_ b: Bool) -> Sexp { b ? t : .nil } | ||
| 60 | static func list(_ items: [Sexp]) -> Sexp { items.isEmpty ? .nil : .list(items) } | ||
| 61 | |||
| 62 | static func cons(_ a: Sexp, _ b: Sexp) -> Sexp { | ||
| 63 | switch b { | ||
| 64 | case .list(let items): .list([a] + items) | ||
| 65 | case .dotted(let items, let last): .dotted([a] + items, last) | ||
| 66 | case _ where isNil(b): .list([a]) | ||
| 67 | default: .dotted([a], b) | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | static func car(_ v: Sexp) throws -> Sexp { | ||
| 72 | switch v { | ||
| 73 | case .list(let items): items.first ?? .nil | ||
| 74 | case .dotted(let items, _): items[0] | ||
| 75 | case _ where isNil(v): .nil | ||
| 76 | default: throw Signal(message: "Wrong type argument: listp, \(v.description)") | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | static func cdr(_ v: Sexp) throws -> Sexp { | ||
| 81 | switch v { | ||
| 82 | case .list(let items): list(Array(items.dropFirst())) | ||
| 83 | case .dotted(let items, let last): items.count == 1 ? last : .dotted(Array(items.dropFirst()), last) | ||
| 84 | case _ where isNil(v): .nil | ||
| 85 | default: throw Signal(message: "Wrong type argument: listp, \(v.description)") | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | /// A proper list's elements. | ||
| 90 | static func elements(_ v: Sexp) throws -> [Sexp] { | ||
| 91 | if case .list(let items) = v { return items } | ||
| 92 | if isNil(v) { return [] } | ||
| 93 | throw Signal(message: "Wrong type argument: listp, \(v.description)") | ||
| 94 | } | ||
| 95 | |||
| 96 | enum Number { | ||
| 97 | case int(Int), float(Double) | ||
| 98 | var double: Double { | ||
| 99 | switch self { | ||
| 100 | case .int(let i): Double(i) | ||
| 101 | case .float(let d): d | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | static func number(_ v: Sexp) throws -> Number { | ||
| 107 | switch v { | ||
| 108 | case .integer(let i): return .int(i) | ||
| 109 | case .float(let d): return .float(d) | ||
| 110 | case .character(let c): return .int(c) | ||
| 111 | default: throw Signal(message: "Wrong type argument: number-or-marker-p, \(v.description)") | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | static func int(_ v: Sexp) throws -> Int { | ||
| 116 | switch v { | ||
| 117 | case .integer(let i): return i | ||
| 118 | case .character(let c): return c | ||
| 119 | default: throw Signal(message: "Wrong type argument: integerp, \(v.description)") | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | static func string(_ v: Sexp) throws -> String { | ||
| 124 | guard case .string(let s) = v else { throw Signal(message: "Wrong type argument: stringp, \(v.description)") } | ||
| 125 | return s | ||
| 126 | } | ||
| 127 | |||
| 128 | static func sexp(_ n: Number) -> Sexp { | ||
| 129 | switch n { | ||
| 130 | case .int(let i): .integer(i) | ||
| 131 | case .float(let d): .float(d) | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | /// `prin1-to-string` with `princ` for strings when `escape` is false. | ||
| 136 | public static func printed(_ v: Sexp, escape: Bool = false) -> String { | ||
| 137 | switch v { | ||
| 138 | case .string(let s): return escape ? v.description : s | ||
| 139 | case .float(let d): return floatText(d) | ||
| 140 | case .character(let c): return String(c) | ||
| 141 | case .list(let items): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + ")" | ||
| 142 | case .dotted(let items, let last): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + " . " + printed(last, escape: true) + ")" | ||
| 143 | case .vector(let items): return "[" + items.map { printed($0, escape: true) }.joined(separator: " ") + "]" | ||
| 144 | default: return v.description | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | /// Emacs's float printing: the shortest text that reads back, with `.0` on whole numbers. | ||
| 149 | static func floatText(_ d: Double) -> String { | ||
| 150 | if d.isNaN { return d.sign == .minus ? "-0.0e+NaN" : "0.0e+NaN" } | ||
| 151 | if d.isInfinite { return d < 0 ? "-1.0e+INF" : "1.0e+INF" } | ||
| 152 | var text = "\(d)" | ||
| 153 | if let e = text.firstIndex(of: "e") { | ||
| 154 | let mantissa = text[..<e] | ||
| 155 | var exponent = String(text[text.index(after: e)...]) | ||
| 156 | if !exponent.hasPrefix("-"), !exponent.hasPrefix("+") { exponent = "+" + exponent } | ||
| 157 | text = mantissa.hasSuffix(".0") ? String(mantissa.dropLast(2)) + "e" + exponent : mantissa + "e" + exponent | ||
| 158 | } | ||
| 159 | return text | ||
| 160 | } | ||
| 161 | |||
| 162 | // MARK: - Evaluation | ||
| 163 | |||
| 164 | public func eval(_ form: Sexp) throws -> Sexp { | ||
| 165 | steps += 1 | ||
| 166 | if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") } | ||
| 167 | switch form { | ||
| 168 | case .symbol(let name): | ||
| 169 | if name == "nil" || name == "t" || name.hasPrefix(":") { return form } | ||
| 170 | return try value(name) | ||
| 171 | case .character(let c): return .integer(c) | ||
| 172 | case .list(let items): | ||
| 173 | guard let head = items.first else { return .nil } | ||
| 174 | guard case .symbol(let name) = head else { | ||
| 175 | if case .list(let lambda) = head, lambda.first == .symbol("lambda") { | ||
| 176 | return try call(head, try items.dropFirst().map(eval)) | ||
| 177 | } | ||
| 178 | throw Signal(message: "Invalid function: \(head.description)") | ||
| 179 | } | ||
| 180 | return try special(name, Array(items.dropFirst())) ?? callNamed(name, try items.dropFirst().map(eval)) | ||
| 181 | case .dotted: throw Signal(message: "Invalid function") | ||
| 182 | default: return form | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 186 | func progn(_ body: some Collection<Sexp>) throws -> Sexp { | ||
| 187 | var result = Sexp.nil | ||
| 188 | for form in body { result = try eval(form) } | ||
| 189 | return result | ||
| 190 | } | ||
| 191 | |||
| 192 | /// Special forms and macros; nil when `name` is an ordinary function. | ||
| 193 | private func special(_ name: String, _ args: [Sexp]) throws -> Sexp? { | ||
| 194 | switch name { | ||
| 195 | case "quote": | ||
| 196 | return args.first ?? .nil | ||
| 197 | case "function": | ||
| 198 | return args.first ?? .nil | ||
| 199 | case "lambda": | ||
| 200 | return .list([.symbol("lambda")] + args) | ||
| 201 | case "progn": | ||
| 202 | return try progn(args) | ||
| 203 | case "prog1": | ||
| 204 | guard let first = args.first else { return .nil } | ||
| 205 | let value = try eval(first) | ||
| 206 | _ = try progn(args.dropFirst()) | ||
| 207 | return value | ||
| 208 | case "if": | ||
| 209 | guard args.count >= 2 else { throw Signal(message: "Wrong number of arguments: if") } | ||
| 210 | return Self.isNil(try eval(args[0])) ? try progn(args.dropFirst(2)) : try eval(args[1]) | ||
| 211 | case "when", "unless": | ||
| 212 | guard let test = args.first else { return .nil } | ||
| 213 | return Self.isNil(try eval(test)) == (name == "unless") ? try progn(args.dropFirst()) : .nil | ||
| 214 | case "cond": | ||
| 215 | for clause in args { | ||
| 216 | let parts = try Self.elements(clause) | ||
| 217 | guard let test = parts.first else { continue } | ||
| 218 | let value = try eval(test) | ||
| 219 | if !Self.isNil(value) { return parts.count == 1 ? value : try progn(parts.dropFirst()) } | ||
| 220 | } | ||
| 221 | return .nil | ||
| 222 | case "and": | ||
| 223 | var value = Self.t | ||
| 224 | for form in args { | ||
| 225 | value = try eval(form) | ||
| 226 | if Self.isNil(value) { return .nil } | ||
| 227 | } | ||
| 228 | return value | ||
| 229 | case "or": | ||
| 230 | for form in args { | ||
| 231 | let value = try eval(form) | ||
| 232 | if !Self.isNil(value) { return value } | ||
| 233 | } | ||
| 234 | return .nil | ||
| 235 | case "let", "let*": | ||
| 236 | guard let first = args.first else { return .nil } | ||
| 237 | var bindings: [String: Sexp] = [:] | ||
| 238 | scopes.append([:]) | ||
| 239 | defer { scopes.removeLast() } | ||
| 240 | for binding in try Self.elements(first) { | ||
| 241 | let (variable, value): (String, Sexp) | ||
| 242 | if case .symbol(let s) = binding { | ||
| 243 | (variable, value) = (s, .nil) | ||
| 244 | } else { | ||
| 245 | let parts = try Self.elements(binding) | ||
| 246 | guard case .symbol(let s)? = parts.first else { throw Signal(message: "Bad binding") } | ||
| 247 | (variable, value) = (s, parts.count > 1 ? try eval(parts[1]) : .nil) | ||
| 248 | } | ||
| 249 | if name == "let*" { scopes[scopes.count - 1][variable] = value } else { bindings[variable] = value } | ||
| 250 | } | ||
| 251 | if name == "let" { scopes[scopes.count - 1] = bindings } | ||
| 252 | return try progn(args.dropFirst()) | ||
| 253 | case "setq": | ||
| 254 | var value = Sexp.nil | ||
| 255 | var i = 0 | ||
| 256 | while i + 1 < args.count { | ||
| 257 | guard case .symbol(let variable) = args[i] else { throw Signal(message: "Bad setq") } | ||
| 258 | value = try eval(args[i + 1]) | ||
| 259 | assign(variable, value) | ||
| 260 | i += 2 | ||
| 261 | } | ||
| 262 | return value | ||
| 263 | case "push", "pop": | ||
| 264 | guard case .symbol(let variable)? = (name == "push" ? args.dropFirst().first : args.first) else { throw Unsupported(what: "\(name) on a place") } | ||
| 265 | let list = try value(variable) | ||
| 266 | if name == "pop" { | ||
| 267 | assign(variable, try Self.cdr(list)) | ||
| 268 | return try Self.car(list) | ||
| 269 | } | ||
| 270 | let pushed = Self.cons(try eval(args[0]), list) | ||
| 271 | assign(variable, pushed) | ||
| 272 | return pushed | ||
| 273 | case "while": | ||
| 274 | guard let test = args.first else { return .nil } | ||
| 275 | while !Self.isNil(try eval(test)) { _ = try progn(args.dropFirst()) } | ||
| 276 | return .nil | ||
| 277 | case "dolist", "dotimes": | ||
| 278 | let spec = try args.first.map(Self.elements) ?? [] | ||
| 279 | guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") } | ||
| 280 | let source = try eval(spec[1]) | ||
| 281 | let items = name == "dolist" ? try Self.elements(source) : (0..<max(0, try Self.int(source))).map { Sexp.integer($0) } | ||
| 282 | scopes.append([:]) | ||
| 283 | defer { scopes.removeLast() } | ||
| 284 | for item in items { | ||
| 285 | scopes[scopes.count - 1][variable] = item | ||
| 286 | _ = try progn(args.dropFirst()) | ||
| 287 | } | ||
| 288 | if spec.count > 2 { | ||
| 289 | scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(items.count) : .nil | ||
| 290 | return try eval(spec[2]) | ||
| 291 | } | ||
| 292 | return .nil | ||
| 293 | case "ignore-errors": | ||
| 294 | do { return try progn(args) } catch is Signal { return .nil } | ||
| 295 | case "condition-case": | ||
| 296 | guard args.count >= 2 else { throw Signal(message: "Bad condition-case") } | ||
| 297 | do { | ||
| 298 | return try eval(args[1]) | ||
| 299 | } catch let signal as Signal { | ||
| 300 | for handler in args.dropFirst(2) { | ||
| 301 | let parts = try Self.elements(handler) | ||
| 302 | guard let condition = parts.first else { continue } | ||
| 303 | let names = (try? Self.elements(condition)) ?? [condition] | ||
| 304 | guard names.contains(.symbol("error")) || names.contains(.symbol("t")) else { continue } | ||
| 305 | let bindings: [String: Sexp] = args[0].symbol.map { $0 == "nil" ? [:] : [$0: .list([.symbol("error"), .string(signal.message)])] } ?? [:] | ||
| 306 | return try binding(bindings) { try progn(parts.dropFirst()) } | ||
| 307 | } | ||
| 308 | throw signal | ||
| 309 | } | ||
| 310 | default: | ||
| 311 | return nil | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | /// Calls a function value: a symbol naming one, or a lambda list. | ||
| 316 | public func call(_ function: Sexp, _ args: [Sexp]) throws -> Sexp { | ||
| 317 | switch function { | ||
| 318 | case .symbol(let name): return try callNamed(name, args) | ||
| 319 | case .list(let items) where items.first == .symbol("lambda") && items.count >= 2: | ||
| 320 | var bindings: [String: Sexp] = [:] | ||
| 321 | var mode = "required" | ||
| 322 | var i = 0 | ||
| 323 | for parameter in try Self.elements(items[1]) { | ||
| 324 | guard case .symbol(let p) = parameter else { throw Signal(message: "Bad lambda list") } | ||
| 325 | if p == "&optional" || p == "&rest" { | ||
| 326 | mode = p | ||
| 327 | continue | ||
| 328 | } | ||
| 329 | if mode == "&rest" { | ||
| 330 | bindings[p] = Self.list(Array(args.dropFirst(i))) | ||
| 331 | i = args.count | ||
| 332 | } else if i < args.count { | ||
| 333 | bindings[p] = args[i] | ||
| 334 | i += 1 | ||
| 335 | } else if mode == "&optional" { | ||
| 336 | bindings[p] = .nil | ||
| 337 | } else { | ||
| 338 | throw Signal(message: "Wrong number of arguments") | ||
| 339 | } | ||
| 340 | } | ||
| 341 | if i < args.count { throw Signal(message: "Wrong number of arguments") } | ||
| 342 | return try binding(bindings) { try progn(items.dropFirst(2)) } | ||
| 343 | default: | ||
| 344 | throw Signal(message: "Invalid function: \(function.description)") | ||
| 345 | } | ||
| 346 | } | ||
| 347 | |||
| 348 | func callNamed(_ name: String, _ args: [Sexp]) throws -> Sexp { | ||
| 349 | guard let function = functions[name] else { throw Unsupported(what: "function \(name)") } | ||
| 350 | return try function(self, args) | ||
| 351 | } | ||
| 352 | |||
| 353 | static func arity(_ args: [Sexp], _ range: ClosedRange<Int>, _ name: String) throws { | ||
| 354 | guard range.contains(args.count) else { throw Signal(message: "Wrong number of arguments: \(name), \(args.count)") } | ||
| 355 | } | ||
| 356 | |||
| 357 | // MARK: - Functions | ||
| 358 | |||
| 359 | private static func core(_ lisp: Elisp) { | ||
| 360 | func arithmetic(_ name: String, identity: Int, _ intOp: @escaping (Int, Int) -> (Int, Bool), _ floatOp: @escaping (Double, Double) -> Double) { | ||
| 361 | lisp.define(name) { _, args in | ||
| 362 | let numbers = try args.map(number) | ||
| 363 | guard var result = numbers.first else { return .integer(identity) } | ||
| 364 | if numbers.count == 1, name == "-" { | ||
| 365 | switch result { | ||
| 366 | case .int(let i): return .integer(-i) | ||
| 367 | case .float(let d): return .float(-d) | ||
| 368 | } | ||
| 369 | } | ||
| 370 | for n in numbers.dropFirst() { | ||
| 371 | switch (result, n) { | ||
| 372 | case (.int(let a), .int(let b)): | ||
| 373 | let (value, overflow) = intOp(a, b) | ||
| 374 | if overflow { throw Unsupported(what: "integer size") } | ||
| 375 | result = .int(value) | ||
| 376 | default: | ||
| 377 | result = .float(floatOp(result.double, n.double)) | ||
| 378 | } | ||
| 379 | } | ||
| 380 | return sexp(result) | ||
| 381 | } | ||
| 382 | } | ||
| 383 | arithmetic("+", identity: 0, { $0.addingReportingOverflow($1) }, +) | ||
| 384 | arithmetic("-", identity: 0, { $0.subtractingReportingOverflow($1) }, -) | ||
| 385 | arithmetic("*", identity: 1, { $0.multipliedReportingOverflow(by: $1) }, *) | ||
| 386 | lisp.define("/") { _, args in | ||
| 387 | try arity(args, 1...Int.max, "/") | ||
| 388 | let numbers = try args.map(number) | ||
| 389 | let floating = numbers.contains { if case .float = $0 { true } else { false } } | ||
| 390 | if numbers.count == 1 { return try sexp(divide(.int(1), numbers[0], floating: floating)) } | ||
| 391 | var result = numbers[0] | ||
| 392 | for n in numbers.dropFirst() { result = try divide(result, n, floating: floating) } | ||
| 393 | return sexp(result) | ||
| 394 | } | ||
| 395 | lisp.define("%") { _, args in | ||
| 396 | try arity(args, 2...2, "%") | ||
| 397 | let b = try int(args[1]) | ||
| 398 | guard b != 0 else { throw Signal(message: "Arithmetic error") } | ||
| 399 | return .integer(try int(args[0]) % b) | ||
| 400 | } | ||
| 401 | lisp.define("mod") { _, args in | ||
| 402 | try arity(args, 2...2, "mod") | ||
| 403 | switch (try number(args[0]), try number(args[1])) { | ||
| 404 | case (.int(let a), .int(let b)): | ||
| 405 | guard b != 0 else { throw Signal(message: "Arithmetic error") } | ||
| 406 | let r = a % b | ||
| 407 | return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r) | ||
| 408 | case (let a, let b): | ||
| 409 | let r = fmod(a.double, b.double) | ||
| 410 | return .float(r != 0 && (r < 0) != (b.double < 0) ? r + b.double : r) | ||
| 411 | } | ||
| 412 | } | ||
| 413 | lisp.define("1+") { _, args in try arity(args, 1...1, "1+"); return try sexp(add(number(args[0]), 1)) } | ||
| 414 | lisp.define("1-") { _, args in try arity(args, 1...1, "1-"); return try sexp(add(number(args[0]), -1)) } | ||
| 415 | lisp.define("abs") { _, args in | ||
| 416 | try arity(args, 1...1, "abs") | ||
| 417 | switch try number(args[0]) { | ||
| 418 | case .int(let i): return .integer(abs(i)) | ||
| 419 | case .float(let d): return .float(abs(d)) | ||
| 420 | } | ||
| 421 | } | ||
| 422 | for name in ["max", "min"] { | ||
| 423 | lisp.define(name) { _, args in | ||
| 424 | try arity(args, 1...Int.max, name) | ||
| 425 | let numbers = try args.map(number) | ||
| 426 | let floating = numbers.contains { if case .float = $0 { true } else { false } } | ||
| 427 | var best = numbers[0] | ||
| 428 | for n in numbers.dropFirst() where name == "max" ? n.double > best.double : n.double < best.double { best = n } | ||
| 429 | return floating ? .float(best.double) : sexp(best) | ||
| 430 | } | ||
| 431 | } | ||
| 432 | for name in ["floor", "ceiling", "round", "truncate"] { | ||
| 433 | lisp.define(name) { _, args in | ||
| 434 | try arity(args, 1...2, name) | ||
| 435 | var x = try number(args[0]).double | ||
| 436 | if args.count == 2, !isNil(args[1]) { | ||
| 437 | let divisor = try number(args[1]).double | ||
| 438 | guard divisor != 0 else { throw Signal(message: "Arithmetic error") } | ||
| 439 | x /= divisor | ||
| 440 | } else if case .int(let i) = try number(args[0]) { | ||
| 441 | return .integer(i) | ||
| 442 | } | ||
| 443 | let rule: FloatingPointRoundingRule = switch name { | ||
| 444 | case "floor": .down | ||
| 445 | case "ceiling": .up | ||
| 446 | case "round": .toNearestOrEven | ||
| 447 | default: .towardZero | ||
| 448 | } | ||
| 449 | guard let i = Int(exactly: x.rounded(rule)) else { throw Signal(message: "Arithmetic overflow error") } | ||
| 450 | return .integer(i) | ||
| 451 | } | ||
| 452 | } | ||
| 453 | lisp.define("float") { _, args in try arity(args, 1...1, "float"); return .float(try number(args[0]).double) } | ||
| 454 | for (name, op) in [("=", { (a: Double, b: Double) in a == b }), ("<", { $0 < $1 }), (">", { $0 > $1 }), ("<=", { $0 <= $1 }), (">=", { $0 >= $1 })] { | ||
| 455 | lisp.define(name) { _, args in | ||
| 456 | try arity(args, 1...Int.max, name) | ||
| 457 | let numbers = try args.map(number) | ||
| 458 | for (a, b) in zip(numbers, numbers.dropFirst()) { | ||
| 459 | let holds: Bool = switch (a, b) { | ||
| 460 | case (.int(let x), .int(let y)): op(Double(x), Double(y)) && (name != "=" || x == y) | ||
| 461 | default: op(a.double, b.double) | ||
| 462 | } | ||
| 463 | if !holds { return .nil } | ||
| 464 | } | ||
| 465 | return t | ||
| 466 | } | ||
| 467 | } | ||
| 468 | lisp.define("/=") { _, args in | ||
| 469 | try arity(args, 2...2, "/=") | ||
| 470 | return bool(try number(args[0]).double != (try number(args[1])).double) | ||
| 471 | } | ||
| 472 | lisp.define("zerop") { _, args in try arity(args, 1...1, "zerop"); return bool(try number(args[0]).double == 0) } | ||
| 473 | lisp.define("not") { _, args in try arity(args, 1...1, "not"); return bool(isNil(args[0])) } | ||
| 474 | lisp.define("null") { _, args in try arity(args, 1...1, "null"); return bool(isNil(args[0])) } | ||
| 475 | lisp.define("eq") { _, args in try arity(args, 2...2, "eq"); return bool(eq(args[0], args[1])) } | ||
| 476 | lisp.define("eql") { _, args in try arity(args, 2...2, "eql"); return bool(eq(args[0], args[1])) } | ||
| 477 | lisp.define("equal") { _, args in try arity(args, 2...2, "equal"); return bool(normalized(args[0]) == normalized(args[1])) } | ||
| 478 | lisp.define("numberp") { _, args in | ||
| 479 | try arity(args, 1...1, "numberp") | ||
| 480 | return bool((try? number(args[0])) != nil && args[0].character == nil) | ||
| 481 | } | ||
| 482 | lisp.define("integerp") { _, args in try arity(args, 1...1, "integerp"); return bool(args[0].integer != nil) } | ||
| 483 | lisp.define("floatp") { _, args in | ||
| 484 | try arity(args, 1...1, "floatp") | ||
| 485 | if case .float = args[0] { return t } | ||
| 486 | return .nil | ||
| 487 | } | ||
| 488 | lisp.define("stringp") { _, args in try arity(args, 1...1, "stringp"); return bool(args[0].string != nil) } | ||
| 489 | lisp.define("listp") { _, args in | ||
| 490 | try arity(args, 1...1, "listp") | ||
| 491 | switch args[0] { | ||
| 492 | case .list, .dotted: return t | ||
| 493 | default: return bool(isNil(args[0])) | ||
| 494 | } | ||
| 495 | } | ||
| 496 | lisp.define("consp") { _, args in | ||
| 497 | try arity(args, 1...1, "consp") | ||
| 498 | switch args[0] { | ||
| 499 | case .list(let items): return bool(!items.isEmpty) | ||
| 500 | case .dotted: return t | ||
| 501 | default: return .nil | ||
| 502 | } | ||
| 503 | } | ||
| 504 | lisp.define("symbolp") { _, args in try arity(args, 1...1, "symbolp"); return bool(args[0].symbol != nil || isNil(args[0])) } | ||
| 505 | |||
| 506 | // Lists. | ||
| 507 | lisp.define("car") { _, args in try arity(args, 1...1, "car"); return try car(args[0]) } | ||
| 508 | lisp.define("cdr") { _, args in try arity(args, 1...1, "cdr"); return try cdr(args[0]) } | ||
| 509 | lisp.define("cadr") { _, args in try arity(args, 1...1, "cadr"); return try car(cdr(args[0])) } | ||
| 510 | lisp.define("cddr") { _, args in try arity(args, 1...1, "cddr"); return try cdr(cdr(args[0])) } | ||
| 511 | lisp.define("cons") { _, args in try arity(args, 2...2, "cons"); return cons(args[0], args[1]) } | ||
| 512 | lisp.define("list") { _, args in list(args) } | ||
| 513 | lisp.define("nth") { _, args in | ||
| 514 | try arity(args, 2...2, "nth") | ||
| 515 | var v = args[1] | ||
| 516 | for _ in 0..<max(0, try int(args[0])) { v = try cdr(v) } | ||
| 517 | return try car(v) | ||
| 518 | } | ||
| 519 | lisp.define("nthcdr") { _, args in | ||
| 520 | try arity(args, 2...2, "nthcdr") | ||
| 521 | var v = args[1] | ||
| 522 | for _ in 0..<max(0, try int(args[0])) { v = try cdr(v) } | ||
| 523 | return v | ||
| 524 | } | ||
| 525 | lisp.define("elt") { _, args in | ||
| 526 | try arity(args, 2...2, "elt") | ||
| 527 | let items = try sequence(args[0]) | ||
| 528 | let i = try int(args[1]) | ||
| 529 | if case .list = args[0], i >= items.count { return .nil } | ||
| 530 | guard items.indices.contains(i) else { throw Signal(message: "Args out of range") } | ||
| 531 | return items[i] | ||
| 532 | } | ||
| 533 | lisp.define("aref") { _, args in | ||
| 534 | try arity(args, 2...2, "aref") | ||
| 535 | let items = try sequence(args[0]) | ||
| 536 | let i = try int(args[1]) | ||
| 537 | guard items.indices.contains(i) else { throw Signal(message: "Args out of range") } | ||
| 538 | return items[i] | ||
| 539 | } | ||
| 540 | lisp.define("append") { _, args in | ||
| 541 | guard let last = args.last else { return .nil } | ||
| 542 | var items: [Sexp] = [] | ||
| 543 | for arg in args.dropLast() { items += try sequence(arg) } | ||
| 544 | return try items.reversed().reduce(last) { cons($1, $0) } | ||
| 545 | } | ||
| 546 | lisp.define("length") { _, args in | ||
| 547 | try arity(args, 1...1, "length") | ||
| 548 | return .integer(try sequence(args[0]).count) | ||
| 549 | } | ||
| 550 | lisp.define("reverse") { _, args in | ||
| 551 | try arity(args, 1...1, "reverse") | ||
| 552 | if case .string(let s) = args[0] { return .string(String(s.reversed())) } | ||
| 553 | return list(try elements(args[0]).reversed()) | ||
| 554 | } | ||
| 555 | lisp.define("number-sequence") { _, args in | ||
| 556 | try arity(args, 1...3, "number-sequence") | ||
| 557 | let from = try int(args[0]) | ||
| 558 | guard args.count > 1, !isNil(args[1]) else { return .list([.integer(from)]) } | ||
| 559 | let to = try int(args[1]) | ||
| 560 | let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1 | ||
| 561 | guard step != 0 else { throw Signal(message: "The increment can not be zero") } | ||
| 562 | return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) }) | ||
| 563 | } | ||
| 564 | for name in ["memq", "member", "memql"] { | ||
| 565 | lisp.define(name) { _, args in | ||
| 566 | try arity(args, 2...2, name) | ||
| 567 | var rest = args[1] | ||
| 568 | while case .list(let items) = rest, let first = items.first { | ||
| 569 | if name == "member" ? normalized(first) == normalized(args[0]) : eq(first, args[0]) { return rest } | ||
| 570 | rest = list(Array(items.dropFirst())) | ||
| 571 | } | ||
| 572 | return .nil | ||
| 573 | } | ||
| 574 | } | ||
| 575 | for name in ["assoc", "assq"] { | ||
| 576 | lisp.define(name) { _, args in | ||
| 577 | try arity(args, 2...3, name) | ||
| 578 | for pair in try elements(args[1]) { | ||
| 579 | guard let key = try? car(pair) else { continue } | ||
| 580 | if name == "assoc" ? normalized(key) == normalized(args[0]) : eq(key, args[0]) { return pair } | ||
| 581 | } | ||
| 582 | return .nil | ||
| 583 | } | ||
| 584 | } | ||
| 585 | lisp.define("identity") { _, args in try arity(args, 1...1, "identity"); return args[0] } | ||
| 586 | lisp.define("ignore") { _, _ in .nil } | ||
| 587 | lisp.define("funcall") { lisp, args in | ||
| 588 | try arity(args, 1...Int.max, "funcall") | ||
| 589 | return try lisp.call(args[0], Array(args.dropFirst())) | ||
| 590 | } | ||
| 591 | lisp.define("apply") { lisp, args in | ||
| 592 | try arity(args, 1...Int.max, "apply") | ||
| 593 | let spread = args.count > 1 ? Array(args[1..<(args.count - 1)]) + (try elements(args[args.count - 1])) : [] | ||
| 594 | return try lisp.call(args[0], spread) | ||
| 595 | } | ||
| 596 | lisp.define("mapcar") { lisp, args in | ||
| 597 | try arity(args, 2...2, "mapcar") | ||
| 598 | return list(try sequence(args[1]).map { try lisp.call(args[0], [$0]) }) | ||
| 599 | } | ||
| 600 | lisp.define("mapc") { lisp, args in | ||
| 601 | try arity(args, 2...2, "mapc") | ||
| 602 | for item in try sequence(args[1]) { _ = try lisp.call(args[0], [item]) } | ||
| 603 | return args[1] | ||
| 604 | } | ||
| 605 | lisp.define("mapconcat") { lisp, args in | ||
| 606 | try arity(args, 1...3, "mapconcat") | ||
| 607 | let separator = args.count > 2 ? try string(args[2]) : "" | ||
| 608 | return .string(try sequence(args[1]).map { try printedString(lisp.call(args[0], [$0])) }.joined(separator: separator)) | ||
| 609 | } | ||
| 610 | lisp.define("delq") { _, args in | ||
| 611 | try arity(args, 2...2, "delq") | ||
| 612 | return list(try elements(args[1]).filter { !eq($0, args[0]) }) | ||
| 613 | } | ||
| 614 | lisp.define("delete") { _, args in | ||
| 615 | try arity(args, 2...2, "delete") | ||
| 616 | return list(try elements(args[1]).filter { normalized($0) != normalized(args[0]) }) | ||
| 617 | } | ||
| 618 | lisp.define("error") { _, args in | ||
| 619 | let message = args.isEmpty ? "" : try format(args) | ||
| 620 | throw Signal(message: message) | ||
| 621 | } | ||
| 622 | lisp.define("user-error") { _, args in | ||
| 623 | throw Signal(message: args.isEmpty ? "" : try format(args)) | ||
| 624 | } | ||
| 625 | |||
| 626 | // Strings. | ||
| 627 | lisp.define("concat") { _, args in .string(try args.map(printedString).joined()) } | ||
| 628 | lisp.define("format") { _, args in .string(try format(args)) } | ||
| 629 | lisp.define("format-message") { _, args in .string(try format(args)) } | ||
| 630 | lisp.define("string=") { _, args in | ||
| 631 | try arity(args, 2...2, "string=") | ||
| 632 | return bool(try stringOrSymbol(args[0]) == (try stringOrSymbol(args[1]))) | ||
| 633 | } | ||
| 634 | lisp.define("string-equal") { _, args in | ||
| 635 | try arity(args, 2...2, "string-equal") | ||
| 636 | return bool(try stringOrSymbol(args[0]) == (try stringOrSymbol(args[1]))) | ||
| 637 | } | ||
| 638 | lisp.define("string<") { _, args in | ||
| 639 | try arity(args, 2...2, "string<") | ||
| 640 | return bool(Array(try stringOrSymbol(args[0]).unicodeScalars.map(\.value)).lexicographicallyPrecedes(try stringOrSymbol(args[1]).unicodeScalars.map(\.value))) | ||
| 641 | } | ||
| 642 | lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) } | ||
| 643 | lisp.define("upcase") { _, args in | ||
| 644 | try arity(args, 1...1, "upcase") | ||
| 645 | if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).uppercased().unicodeScalars.first!.value } ?? UInt32(c))) } | ||
| 646 | return .string(try string(args[0]).uppercased()) | ||
| 647 | } | ||
| 648 | lisp.define("downcase") { _, args in | ||
| 649 | try arity(args, 1...1, "downcase") | ||
| 650 | if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).lowercased().unicodeScalars.first!.value } ?? UInt32(c))) } | ||
| 651 | return .string(try string(args[0]).lowercased()) | ||
| 652 | } | ||
| 653 | lisp.define("capitalize") { _, args in | ||
| 654 | try arity(args, 1...1, "capitalize") | ||
| 655 | return .string(try string(args[0]).capitalized) | ||
| 656 | } | ||
| 657 | lisp.define("substring") { _, args in | ||
| 658 | try arity(args, 1...3, "substring") | ||
| 659 | let chars = Array(try string(args[0])) | ||
| 660 | func index(_ v: Sexp?, _ fallback: Int) throws -> Int { | ||
| 661 | guard let v, !isNil(v) else { return fallback } | ||
| 662 | let i = try int(v) | ||
| 663 | return i < 0 ? chars.count + i : i | ||
| 664 | } | ||
| 665 | let from = try index(args.count > 1 ? args[1] : nil, 0) | ||
| 666 | let to = try index(args.count > 2 ? args[2] : nil, chars.count) | ||
| 667 | guard from >= 0, to <= chars.count, from <= to else { throw Signal(message: "Args out of range") } | ||
| 668 | return .string(String(chars[from..<to])) | ||
| 669 | } | ||
| 670 | lisp.define("string-to-number") { _, args in | ||
| 671 | try arity(args, 1...2, "string-to-number") | ||
| 672 | return stringToNumber(try string(args[0])) | ||
| 673 | } | ||
| 674 | for name in ["number-to-string", "int-to-string"] { | ||
| 675 | lisp.define(name) { _, args in | ||
| 676 | try arity(args, 1...1, name) | ||
| 677 | return .string(printed(sexp(try number(args[0])))) | ||
| 678 | } | ||
| 679 | } | ||
| 680 | lisp.define("string-prefix-p") { _, args in | ||
| 681 | try arity(args, 2...3, "string-prefix-p") | ||
| 682 | return bool(try string(args[1]).hasPrefix(try string(args[0]))) | ||
| 683 | } | ||
| 684 | lisp.define("string-suffix-p") { _, args in | ||
| 685 | try arity(args, 2...3, "string-suffix-p") | ||
| 686 | return bool(try string(args[1]).hasSuffix(try string(args[0]))) | ||
| 687 | } | ||
| 688 | lisp.define("string-empty-p") { _, args in | ||
| 689 | try arity(args, 1...1, "string-empty-p") | ||
| 690 | return bool(try stringOrSymbol(args[0]).isEmpty) | ||
| 691 | } | ||
| 692 | lisp.define("string-trim") { _, args in | ||
| 693 | try arity(args, 1...1, "string-trim") | ||
| 694 | return .string(try string(args[0]).trimmingCharacters(in: .whitespacesAndNewlines)) | ||
| 695 | } | ||
| 696 | lisp.define("split-string") { _, args in | ||
| 697 | try arity(args, 1...2, "split-string") | ||
| 698 | let s = try string(args[0]) | ||
| 699 | if args.count == 2, !isNil(args[1]) { | ||
| 700 | let separator = try string(args[1]) | ||
| 701 | let regex = try NSRegularExpression(pattern: separator) | ||
| 702 | let ns = s as NSString | ||
| 703 | var parts: [Sexp] = [] | ||
| 704 | var start = 0 | ||
| 705 | for m in regex.matches(in: s, range: NSRange(location: 0, length: ns.length)) where m.range.length > 0 { | ||
| 706 | parts.append(.string(ns.substring(with: NSRange(location: start, length: m.range.location - start)))) | ||
| 707 | start = NSMaxRange(m.range) | ||
| 708 | } | ||
| 709 | parts.append(.string(ns.substring(from: start))) | ||
| 710 | return list(parts) | ||
| 711 | } | ||
| 712 | return list(s.split(whereSeparator: { " \t\n\r\u{0C}\u{0B}".contains($0) }).map { .string(String($0)) }) | ||
| 713 | } | ||
| 714 | } | ||
| 715 | |||
| 716 | static func add(_ n: Number, _ k: Int) -> Number { | ||
| 717 | switch n { | ||
| 718 | case .int(let i): .int(i + k) | ||
| 719 | case .float(let d): .float(d + Double(k)) | ||
| 720 | } | ||
| 721 | } | ||
| 722 | |||
| 723 | static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number { | ||
| 724 | if !floating, case .int(let x) = a, case .int(let y) = b { | ||
| 725 | guard y != 0 else { throw Signal(message: "Arithmetic error") } | ||
| 726 | return .int(x / y) | ||
| 727 | } | ||
| 728 | return .float(a.double / b.double) | ||
| 729 | } | ||
| 730 | |||
| 731 | static func eq(_ a: Sexp, _ b: Sexp) -> Bool { | ||
| 732 | switch (normalized(a), normalized(b)) { | ||
| 733 | case (.symbol(let x), .symbol(let y)): x == y | ||
| 734 | case (.integer(let x), .integer(let y)): x == y | ||
| 735 | case (.float(let x), .float(let y)): x == y | ||
| 736 | default: false | ||
| 737 | } | ||
| 738 | } | ||
| 739 | |||
| 740 | /// Characters as integers and the empty list as nil, for comparing. | ||
| 741 | static func normalized(_ v: Sexp) -> Sexp { | ||
| 742 | switch v { | ||
| 743 | case .character(let c): .integer(c) | ||
| 744 | case .list(let items): items.isEmpty ? .nil : .list(items.map(normalized)) | ||
| 745 | case .dotted(let items, let last): .dotted(items.map(normalized), normalized(last)) | ||
| 746 | case .vector(let items): .vector(items.map(normalized)) | ||
| 747 | default: v | ||
| 748 | } | ||
| 749 | } | ||
| 750 | |||
| 751 | static func sequence(_ v: Sexp) throws -> [Sexp] { | ||
| 752 | switch v { | ||
| 753 | case .string(let s): return s.unicodeScalars.map { .integer(Int($0.value)) } | ||
| 754 | case .vector(let items): return items | ||
| 755 | default: return try elements(v) | ||
| 756 | } | ||
| 757 | } | ||
| 758 | |||
| 759 | static func stringOrSymbol(_ v: Sexp) throws -> String { | ||
| 760 | if case .symbol(let s) = v { return s } | ||
| 761 | return try string(v) | ||
| 762 | } | ||
| 763 | |||
| 764 | /// What `concat` and `mapconcat` accept: strings, and lists or vectors of characters. | ||
| 765 | static func printedString(_ v: Sexp) throws -> String { | ||
| 766 | switch v { | ||
| 767 | case .string(let s): return s | ||
| 768 | case _ where isNil(v): return "" | ||
| 769 | case .list, .vector: | ||
| 770 | return String(String.UnicodeScalarView(try sequence(v).map { Unicode.Scalar(UInt32(try int($0))) ?? " " })) | ||
| 771 | default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)") | ||
| 772 | } | ||
| 773 | } | ||
| 774 | |||
| 775 | /// `string-to-number` in base 10. | ||
| 776 | static func stringToNumber(_ s: String) -> Sexp { | ||
| 777 | let trimmed = s.drop { $0 == " " || $0 == "\t" || $0 == "\n" } | ||
| 778 | guard let r = trimmed.range(of: "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?", options: .regularExpression) else { return .integer(0) } | ||
| 779 | let text = String(trimmed[r]) | ||
| 780 | if text.range(of: "^[-+]?[0-9]+$", options: .regularExpression) != nil, let i = Int(text.hasPrefix("+") ? String(text.dropFirst()) : text) { | ||
| 781 | return .integer(i) | ||
| 782 | } | ||
| 783 | if text.range(of: "^[-+]?[0-9]+\\.$", options: .regularExpression) != nil, let i = Int(text.dropLast().replacingOccurrences(of: "+", with: "")) { | ||
| 784 | return .integer(i) | ||
| 785 | } | ||
| 786 | return .float(Double(text) ?? 0) | ||
| 787 | } | ||
| 788 | |||
| 789 | /// `format`. | ||
| 790 | static func format(_ args: [Sexp]) throws -> String { | ||
| 791 | let spec = Array(try string(args[0])) | ||
| 792 | var out = "" | ||
| 793 | var next = 1 | ||
| 794 | var i = 0 | ||
| 795 | func argument() throws -> Sexp { | ||
| 796 | guard next < args.count else { throw Signal(message: "Not enough arguments for format string") } | ||
| 797 | defer { next += 1 } | ||
| 798 | return args[next] | ||
| 799 | } | ||
| 800 | while i < spec.count { | ||
| 801 | let c = spec[i] | ||
| 802 | i += 1 | ||
| 803 | guard c == "%" else { | ||
| 804 | out.append(c) | ||
| 805 | continue | ||
| 806 | } | ||
| 807 | var flags = "" | ||
| 808 | while i < spec.count, "-+ #0".contains(spec[i]) { | ||
| 809 | flags.append(spec[i]) | ||
| 810 | i += 1 | ||
| 811 | } | ||
| 812 | var width = "" | ||
| 813 | while i < spec.count, spec[i].isNumber { | ||
| 814 | width.append(spec[i]) | ||
| 815 | i += 1 | ||
| 816 | } | ||
| 817 | var precision: Int? | ||
| 818 | if i < spec.count, spec[i] == "." { | ||
| 819 | i += 1 | ||
| 820 | var digits = "" | ||
| 821 | while i < spec.count, spec[i].isNumber { | ||
| 822 | digits.append(spec[i]) | ||
| 823 | i += 1 | ||
| 824 | } | ||
| 825 | precision = Int(digits) ?? 0 | ||
| 826 | } | ||
| 827 | guard i < spec.count else { throw Signal(message: "Format string ends in middle of format specifier") } | ||
| 828 | let conversion = spec[i] | ||
| 829 | i += 1 | ||
| 830 | var text: String | ||
| 831 | switch conversion { | ||
| 832 | case "%": | ||
| 833 | out.append("%") | ||
| 834 | continue | ||
| 835 | case "s", "S": | ||
| 836 | text = printed(try argument(), escape: conversion == "S") | ||
| 837 | if let precision { text = String(text.prefix(precision)) } | ||
| 838 | case "d", "o", "x", "X": | ||
| 839 | let value: Int | ||
| 840 | switch try number(try argument()) { | ||
| 841 | case .int(let n): value = n | ||
| 842 | case .float(let d): | ||
| 843 | guard let n = Int(exactly: d.rounded(.towardZero)) else { throw Signal(message: "Arithmetic overflow") } | ||
| 844 | value = n | ||
| 845 | } | ||
| 846 | text = String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + (conversion == "d" ? "ld" : "l" + String(conversion)), value) | ||
| 847 | out += text | ||
| 848 | continue | ||
| 849 | case "c": | ||
| 850 | text = Unicode.Scalar(UInt32(try int(try argument()))).map { String($0) } ?? "" | ||
| 851 | case "e", "f", "g": | ||
| 852 | let value = try number(try argument()).double | ||
| 853 | out += String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + String(conversion), value) | ||
| 854 | continue | ||
| 855 | default: | ||
| 856 | throw Signal(message: "Invalid format operation %\(conversion)") | ||
| 857 | } | ||
| 858 | let pad = max(0, (Int(width) ?? 0) - text.count) | ||
| 859 | out += flags.contains("-") ? text + String(repeating: " ", count: pad) : String(repeating: " ", count: pad) + text | ||
| 860 | } | ||
| 861 | return out | ||
| 862 | } | ||
| 863 | } | ||
| 864 | |||
| 865 | extension Sexp { | ||
| 866 | var character: Int? { if case .character(let c) = self { return c } else { return nil } } | ||
| 867 | } | ||
| 868 | |||
| 869 | extension LispReader { | ||
| 870 | /// The first form in `text` and the number of characters it spans, as `forward-sexp` | ||
| 871 | /// would read it. | ||
| 872 | public static func readFirst(_ text: String) throws -> (sexp: Sexp, length: Int) { | ||
| 873 | var reader = Reader(Array(text.unicodeScalars)) | ||
| 874 | let sexp = try reader.form() | ||
| 875 | let consumed = String(String.UnicodeScalarView(reader.chars[..<reader.index])) | ||
| 876 | return (sexp, consumed.count) | ||
| 877 | } | ||
| 878 | } | ||
Sources/OrgCore/Parser/Inline.swift +13 −1
| @@ -243,7 +243,7 @@ struct InlineScanner { | |||
| 243 | if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) } | 243 | if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) } |
| 244 | if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) } | 244 | if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) } |
| 245 | case "<": | 245 | case "<": |
| 246 | if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) } | 246 | if !inLink, let end = timestampEnd(chars, at: i, limit: limit) ?? diaryTimestamp(i, limit) { return .object(.timestamp, i..<end) } |
| 247 | if let end = radioTarget(i, limit) { return .object(.radioTarget, i..<end) } | 247 | if let end = radioTarget(i, limit) { return .object(.radioTarget, i..<end) } |
| 248 | if let end = target(i, limit) { return .object(.target, i..<end) } | 248 | if let end = target(i, limit) { return .object(.target, i..<end) } |
| 249 | if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) } | 249 | if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) } |
| @@ -625,6 +625,18 @@ struct InlineScanner { | |||
| 625 | } | 625 | } |
| 626 | 626 | ||
| 627 | /// `<<<target>>>`. | 627 | /// `<<<target>>>`. |
| 628 | /// A diary timestamp, `<%%(SEXP)REST>` (`org-element--timestamp-regexp`). It ends at the | ||
| 629 | /// first `]` or `>`, as its raw value does. | ||
| 630 | func diaryTimestamp(_ i: Int, _ limit: Int) -> Int? { | ||
| 631 | guard hasPrefix("<%%(", at: i, limit) else { return nil } | ||
| 632 | var close = i + 4 | ||
| 633 | while close < limit, chars[close] != ">", chars[close] != "\n" { close += 1 } | ||
| 634 | guard close < limit, chars[close] == ">", close > i + 5, chars[(i + 5)..<close].contains(")") else { return nil } | ||
| 635 | var end = i + 1 | ||
| 636 | while chars[end] != "]", chars[end] != ">" { end += 1 } | ||
| 637 | return end + 1 | ||
| 638 | } | ||
| 639 | |||
| 628 | func radioTarget(_ i: Int, _ limit: Int) -> Int? { | 640 | func radioTarget(_ i: Int, _ limit: Int) -> Int? { |
| 629 | guard hasPrefix("<<<", at: i, limit) else { return nil } | 641 | guard hasPrefix("<<<", at: i, limit) else { return nil } |
| 630 | let start = i + 3 | 642 | let start = i + 3 |
Sources/OrgstarMobile/AgendaScreen.swift +1 −1
| @@ -174,7 +174,7 @@ struct AgendaScreen: View { | |||
| 174 | 174 | ||
| 175 | private func row(_ item: AgendaItem) -> some View { | 175 | private func row(_ item: AgendaItem) -> some View { |
| 176 | Group { | 176 | Group { |
| 177 | if let path = item.path, let offset = item.headingOffset { | 177 | if let path = item.path, let offset = item.headingOffset ?? item.markerOffset { |
| 178 | NavigationLink(value: ReaderRoute.offset(path, offset)) { entry(item) } | 178 | NavigationLink(value: ReaderRoute.offset(path, offset)) { entry(item) } |
| 179 | } else { | 179 | } else { |
| 180 | entry(item) | 180 | entry(item) |
Tests/OrgCoreTests/AgendaTests.swift +1 −1
| @@ -148,7 +148,7 @@ enum AgendaOracle { | |||
| 148 | case .timeGrid, .currentTime: "-" | 148 | case .timeGrid, .currentTime: "-" |
| 149 | default: item.kind.rawValue | 149 | default: item.kind.rawValue |
| 150 | } | 150 | } |
| 151 | return "\(normalize(line)) | \(type) \(item.category) \(item.timeOfDay.map(String.init) ?? "-") [\(item.extra)] \(item.urgency) \(item.path ?? "-")@\(item.headingOffset.map(String.init) ?? "-")" | 151 | return "\(normalize(line)) | \(type) \(item.category) \(item.timeOfDay.map(String.init) ?? "-") [\(item.extra)] \(item.urgency) \(item.headingOffset == nil ? "-" : item.path ?? "-")@\(item.headingOffset.map(String.init) ?? "-")" |
| 152 | } | 152 | } |
| 153 | } | 153 | } |
| 154 | 154 | ||
Tests/OrgCoreTests/DiarySexpTests.swift added +74
| @@ -0,0 +1,74 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import OrgCore | ||
| 4 | |||
| 5 | /// Diary sexps in the agenda (`org-agenda-get-sexps`, diary timestamps and planning). | ||
| 6 | struct DiarySexpTests { | ||
| 7 | static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil | ||
| 8 | |||
| 9 | static let file = """ | ||
| 10 | #+FILETAGS: :ft: | ||
| 11 | * Holidays :hol: | ||
| 12 | %%(diary-float t 4 4) Fourth Thursday | ||
| 13 | %%(diary-anniversary 10 7 1990) Birthday %d%s | ||
| 14 | %%(org-anniversary 2000 10 8) Org %d years | ||
| 15 | %%(diary-anniversary 2 29 2000) Leap %d | ||
| 16 | %%(diary-block 10 5 2026 10 7 2026) Conference | ||
| 17 | %%(diary-cyclic 3 10 1 2026) Every third, %d%s time | ||
| 18 | %%(diary-date t 6 t) Sixth of month | ||
| 19 | %%(diary-date '(10 11) '(5 9) 2026) List date | ||
| 20 | %%(diary-remind '(diary-date 10 9 2026) 2) Remind me | ||
| 21 | %%(diary-remind '(diary-date 10 12 2026) -3) Remind range | ||
| 22 | %%(diary-remind '(diary-anniversary 10 14 2000) 7) Week ahead %d | ||
| 23 | %%(org-class 2026 9 1 2026 12 20 2 41) Class on Tuesdays | ||
| 24 | %%(org-block 2026 10 6 2026 10 6) Meeting at 10:30 | ||
| 25 | %%(memq (calendar-day-of-week date) '(1 3 5)) Gym | ||
| 26 | %%(and (= (calendar-extract-day date) 2) "Custom; second part") | ||
| 27 | %%(let ((d (calendar-extract-day date))) (when (= (% d 5) 0) (format "Day %d is a fifth" d))) | ||
| 28 | %%(diary-float 10 1 -1) Last Monday of October | ||
| 29 | %%(diary-float t 0 1 8) First Sunday after the 8th | ||
| 30 | %%(diary-date 10 8 2026) | ||
| 31 | &%%(diary-date 10 9 2026) Ampersand | ||
| 32 | * TODO Task with a diary stamp <%%(diary-float t 2 1)> | ||
| 33 | * Meeting | ||
| 34 | <%%(diary-date t 7 t) 14:00> | ||
| 35 | * Due | ||
| 36 | DEADLINE: <%%(diary-date 10 8 2026)> | ||
| 37 | * Sched | ||
| 38 | SCHEDULED: <%%(diary-float t 5 1)> | ||
| 39 | * COMMENT Skipped tree | ||
| 40 | %%(diary-date t t t) Never shown | ||
| 41 | """ + "\n" | ||
| 42 | |||
| 43 | @Test func evaluatesDiaryFunctions() throws { | ||
| 44 | let day = Days.absolute(year: 2026, month: 10, day: 7) | ||
| 45 | func run(_ text: String, _ entry: String = "E") throws -> [String]? { | ||
| 46 | DiarySexp.entries(try LispReader.readFirst(text).sexp, entry: entry, day: day) | ||
| 47 | } | ||
| 48 | #expect(try run("(diary-anniversary 10 7 1990)", "%d%s") == ["36th"]) | ||
| 49 | #expect(try run("(diary-date 10 8 2026)") == nil) | ||
| 50 | #expect(try run("(diary-float 10 3 1)") == ["E"]) | ||
| 51 | #expect(try run("\"a; b\"") == ["a", "b"]) | ||
| 52 | #expect(try run("(undefined)") == nil) | ||
| 53 | #expect(try run("(diary-ordinal-suffix -3)") == nil) | ||
| 54 | } | ||
| 55 | |||
| 56 | @Test func skipsSexpsBeforeTheFirstHeading() { | ||
| 57 | let source = AgendaSource(path: "a.org", text: "%%(diary-date t t t) Before\n* H\n%%(diary-date t t t) Under\n") | ||
| 58 | let day = Days.absolute(year: 2026, month: 10, day: 7) | ||
| 59 | #expect(Agenda.list([source], start: day, days: 1, today: day, now: (9, 0))[0].items.map(\.text) == ["Under"]) | ||
| 60 | } | ||
| 61 | |||
| 62 | @Test(.enabled(if: oracle)) | ||
| 63 | func agendaMatchesEmacs() throws { | ||
| 64 | let files = [("diary.org", Self.file)] | ||
| 65 | let october = Days.absolute(year: 2026, month: 10, day: 1) | ||
| 66 | let ours = Agenda.list([AgendaSource(path: "diary.org", text: Self.file)], start: october, days: 15, today: october + 4, now: (9, 30)) | ||
| 67 | let kinds = Set(ours.flatMap(\.items).map(\.kind)) | ||
| 68 | #expect(kinds.isSuperset(of: [.sexp, .timestamp, .deadline, .pastScheduled])) | ||
| 69 | try AgendaOracle.compare(files: files, start: october, span: 15, today: october + 4) | ||
| 70 | try AgendaOracle.compare(files: files, start: october + 20, span: 10, today: october + 26) | ||
| 71 | let leap = Days.absolute(year: 2027, month: 2, day: 27) | ||
| 72 | try AgendaOracle.compare(files: files, start: leap, span: 4, today: leap) | ||
| 73 | } | ||
| 74 | } | ||
Tests/OrgCoreTests/ElispTests.swift added +48
| @@ -0,0 +1,48 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import OrgCore | ||
| 4 | |||
| 5 | /// The Lisp evaluator against Emacs: each form's `prin1` text, or `error` when it signals. | ||
| 6 | struct ElispTests { | ||
| 7 | static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil | ||
| 8 | |||
| 9 | static let forms = [ | ||
| 10 | "(+ 1 2 3)", "(- 10)", "(- 10 2.5)", "(* 2 3.0)", "(/ 7 2)", "(/ -7 2)", "(/ 7 2.0)", "(/ 7 0)", "(% -7 2)", "(mod -7 2)", | ||
| 11 | "(mod 5.5 2)", "(1+ 4)", "(abs -3.5)", "(max 1 2.0)", "(min 3 1)", "(floor 2.7)", "(floor -7 2)", "(round 2.5)", | ||
| 12 | "(round 3.5)", "(truncate -2.7)", "(float 3)", "(= 1 1.0)", "(< 1 2 3)", "(< 1 3 2)", "(/= 1 2)", "(zerop 0.0)", | ||
| 13 | "(eq 'a 'a)", "(equal '(1 \"a\") (list 1 \"a\"))", "(null nil)", "(not 0)", "(car '(1 2))", "(cdr '(1 2))", | ||
| 14 | "(cdr '(1 . 2))", "(cons 1 2)", "(cons 1 '(2))", "(nth 2 '(a b c))", "(nth 5 '(a))", "(length \"héllo\")", | ||
| 15 | "(reverse '(1 2 3))", "(append '(1) '(2) 3)", "(number-sequence 1 5 2)", "(memq 'b '(a b c))", "(member \"b\" '(\"a\" \"b\"))", | ||
| 16 | "(assoc \"k\" '((\"k\" . 1)))", "(mapcar #'1+ '(1 2 3))", "(mapconcat #'identity '(\"a\" \"b\") \"-\")", | ||
| 17 | "(apply #'+ 1 '(2 3))", "(funcall (lambda (x &optional y) (list x y)) 1)", "(let ((x 1) (y 2)) (+ x y))", | ||
| 18 | "(let* ((x 1) (y (1+ x))) y)", "(let ((n 0)) (dotimes (i 4) (setq n (+ n i))) n)", "(let (r) (dolist (x '(1 2)) (push x r)) r)", | ||
| 19 | "(cond ((= 1 2) 'a) ((= 1 1) 'b))", "(and 1 2)", "(or nil 3)", "(if nil 1 2 3)", "(when t 1 2)", "(unless t 1)", | ||
| 20 | "(concat \"a\" \"b\" '(99))", "(format \"%d|%5.2f|%s|%S|%-4s|%x|%c|%%\" 42 3.14159 \"s\" \"q\" \"ab\" 255 65)", | ||
| 21 | "(format \"%s %s\" 1.0 100.5)", "(format \"%d\" 2.9)", "(format \"%03d\" 7)", "(format \"%e\" 12345.678)", "(format \"%g\" 0.0001)", | ||
| 22 | "(substring \"hello\" 1 3)", "(substring \"hello\" -3)", "(string-to-number \" 12abc\")", "(string-to-number \"1.5e2\")", | ||
| 23 | "(string-to-number \"3.\")", "(string-to-number \"x\")", "(number-to-string 0.1)", "(number-to-string 1e21)", "(upcase \"ab\")", | ||
| 24 | "(string= \"a\" \"a\")", "(string< \"a\" \"b\")", "(split-string \" a b \")", "(split-string \"a,b,,c\" \",\")", | ||
| 25 | "(string-prefix-p \"ab\" \"abc\")", "(condition-case nil (/ 1 0) (error 'caught))", "(ignore-errors (car 1))", "(car 1)", | ||
| 26 | "(let ((x 0)) (while (< x 5) (setq x (1+ x))) x)", "(elt [1 2 3] 1)", "(aref \"abc\" 1)", "(length [1 2])", "(nthcdr 1 '(1 2 3))", | ||
| 27 | "(delq nil '(1 nil 2))", "(cadr '(1 2 3))", "(identity \"x\")", "(capitalize \"hello world\")", "(string-trim \" x \")", | ||
| 28 | ] | ||
| 29 | |||
| 30 | static func ours(_ form: String) -> String { | ||
| 31 | guard let sexp = try? LispReader.readFirst(form).sexp else { return "read-error" } | ||
| 32 | do { | ||
| 33 | return Elisp.printed(try Elisp().eval(sexp), escape: true) | ||
| 34 | } catch { | ||
| 35 | return "error" | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | @Test(.enabled(if: oracle)) | ||
| 40 | func matchesEmacs() throws { | ||
| 41 | let list = "(list " + Self.forms.map { "(condition-case nil (prin1-to-string \($0)) (error \"error\"))" }.joined(separator: " ") + ")" | ||
| 42 | let emacs = try EmacsOracle.evaluate("", "(let ((print-escape-newlines t)) \(list))") | ||
| 43 | #expect(emacs.count == Self.forms.count) | ||
| 44 | for (form, expected) in zip(Self.forms, emacs) { | ||
| 45 | #expect(Self.ours(form) == expected, "\(form)") | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
Tests/OrgCoreTests/ParserGapsTests.swift +2 −1
| @@ -11,6 +11,7 @@ struct ParserGapsTests { | |||
| 11 | and on @@html:<b>bold</b>@@ or @@latex:\emph{x}@@, not @@x@@ or @@:y@@ or @@html:open. | 11 | and on @@html:<b>bold</b>@@ or @@latex:\emph{x}@@, not @@x@@ or @@:y@@ or @@html:open. |
| 12 | Calls: call_square(4) call_f[:results raw](x=1, y=(2))[:exports both] xcall_no(1) call_f[x] call_(1). | 12 | Calls: call_square(4) call_f[:results raw](x=1, y=(2))[:exports both] xcall_no(1) call_f[x] call_(1). |
| 13 | %%(diary-float t 4 2) Thanksgiving | 13 | %%(diary-float t 4 2) Thanksgiving |
| 14 | Diary <%%(diary-float t 4 2)>, <%%(x) 10:00>, <%%()>, <%%(a]b)>, <%%(open and <%%(y) | ||
| 14 | %%(indented) is text | 15 | %%(indented) is text |
| 15 | 16 | ||
| 16 | +----+-----+ | 17 | +----+-----+ |
| @@ -47,7 +48,7 @@ struct ParserGapsTests { | |||
| 47 | 48 | ||
| 48 | static let kinds: [(lisp: String, ours: SyntaxKind)] = [ | 49 | static let kinds: [(lisp: String, ours: SyntaxKind)] = [ |
| 49 | ("citation", .citation), ("citation-reference", .citationReference), ("export-snippet", .exportSnippet), | 50 | ("citation", .citation), ("citation-reference", .citationReference), ("export-snippet", .exportSnippet), |
| 50 | ("inline-babel-call", .inlineBabelCall), ("diary-sexp", .diarySexp), ("paragraph", .paragraph), ("item", .item), | 51 | ("inline-babel-call", .inlineBabelCall), ("timestamp", .timestamp), ("diary-sexp", .diarySexp), ("paragraph", .paragraph), ("item", .item), |
| 51 | ] | 52 | ] |
| 52 | 53 | ||
| 53 | @Test func objectsAndElementsMatchOrgElement() throws { | 54 | @Test func objectsAndElementsMatchOrgElement() throws { |