Commit b4ec29e3b2
Verified · cmc
Layout: unified · split
Sources/OrgCore/Agenda/DiarySexp.swift +37 −25
| @@ -37,10 +37,17 @@ public enum DiarySexp { | ||
| 37 | 37 | return .list([.integer(date.month), .integer(date.day), .integer(date.year)]) |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | /// A number from Lisp used in date arithmetic, within what that arithmetic handles. | |
| 41 | static func bounded(_ v: Sexp) throws -> Int { | |
| 42 | let i = try Elisp.int(v) | |
| 43 | guard (-100_000_000...100_000_000).contains(i) else { throw Elisp.Unsupported(what: "date number \(i)") } | |
| 44 | return i | |
| 45 | } | |
| 46 | ||
| 40 | 47 | static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) { |
| 41 | 48 | let items = try Elisp.elements(date) |
| 42 | 49 | 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])) | |
| 50 | return (try bounded(items[0]), try bounded(items[1]), try bounded(items[2])) | |
| 44 | 51 | } |
| 45 | 52 | |
| 46 | 53 | static func absolute(_ date: Sexp) throws -> Int { |
| @@ -50,17 +57,18 @@ public enum DiarySexp { | ||
| 50 | 57 | |
| 51 | 58 | static func isLeap(_ year: Int) -> Bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } |
| 52 | 59 | |
| 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] | |
| 60 | static func lastDay(month: Int, year: Int) throws -> Int { | |
| 61 | guard (1...12).contains(month) else { throw Elisp.Signal(message: "Args out of range: \(month)") } | |
| 62 | return month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] | |
| 55 | 63 | } |
| 56 | 64 | |
| 57 | 65 | /// `calendar-nth-named-absday`. |
| 58 | static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) -> Int { | |
| 66 | static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) throws -> Int { | |
| 59 | 67 | func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 } |
| 60 | 68 | if n > 0 { |
| 61 | 69 | return 7 * (n - 1) + onOrBefore(6 + Days.absolute(year: year, month: month, day: day ?? 1)) |
| 62 | 70 | } |
| 63 | return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: day ?? lastDay(month: month, year: year))) | |
| 71 | return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: try day ?? lastDay(month: month, year: year))) | |
| 64 | 72 | } |
| 65 | 73 | |
| 66 | 74 | /// `diary-ordinal-suffix`. |
| @@ -102,7 +110,7 @@ public enum DiarySexp { | ||
| 102 | 110 | } |
| 103 | 111 | lisp.define("calendar-gregorian-from-absolute") { _, args in |
| 104 | 112 | try arity(args, 1...1, "calendar-gregorian-from-absolute") |
| 105 | return gregorian(try Elisp.int(args[0])) | |
| 113 | return gregorian(try bounded(args[0])) | |
| 106 | 114 | } |
| 107 | 115 | lisp.define("calendar-day-of-week") { _, args in |
| 108 | 116 | try arity(args, 1...1, "calendar-day-of-week") |
| @@ -114,7 +122,7 @@ public enum DiarySexp { | ||
| 114 | 122 | } |
| 115 | 123 | lisp.define("calendar-last-day-of-month") { _, args in |
| 116 | 124 | 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]))) | |
| 125 | return .integer(try lastDay(month: try bounded(args[0]), year: try bounded(args[1]))) | |
| 118 | 126 | } |
| 119 | 127 | lisp.define("calendar-date-equal") { _, args in |
| 120 | 128 | try arity(args, 2...2, "calendar-date-equal") |
| @@ -129,15 +137,15 @@ public enum DiarySexp { | ||
| 129 | 137 | } |
| 130 | 138 | lisp.define("calendar-nth-named-absday") { _, args in |
| 131 | 139 | 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)) | |
| 140 | let day = Elisp.isNil(optional(args, 4)) ? nil : try bounded(args[4]) | |
| 141 | return .integer(try nthNamedAbsday(try bounded(args[0]), try bounded(args[1]), try bounded(args[2]), try bounded(args[3]), day)) | |
| 134 | 142 | } |
| 135 | 143 | lisp.define("calendar-nth-named-day") { lisp, args in |
| 136 | gregorian(try Elisp.int(try lisp.callNamed("calendar-nth-named-absday", args))) | |
| 144 | gregorian(try bounded(try lisp.callNamed("calendar-nth-named-absday", args))) | |
| 137 | 145 | } |
| 138 | 146 | lisp.define("calendar-iso-from-absolute") { _, args in |
| 139 | 147 | try arity(args, 1...1, "calendar-iso-from-absolute") |
| 140 | let day = try Elisp.int(args[0]) | |
| 148 | let day = try bounded(args[0]) | |
| 141 | 149 | let thursday = day - (Days.weekday(day) + 6) % 7 + 3 |
| 142 | 150 | return .list([.integer(Days.isoWeek(day)), .integer(Days.weekday(day)), .integer(Days.date(thursday).year)]) |
| 143 | 151 | } |
| @@ -175,13 +183,13 @@ public enum DiarySexp { | ||
| 175 | 183 | lisp.define("diary-float") { _, args in |
| 176 | 184 | try arity(args, 3...5, "diary-float") |
| 177 | 185 | 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]) | |
| 186 | let dayname = try bounded(args[1]) | |
| 187 | let n = try bounded(args[2]) | |
| 188 | let day = Elisp.isNil(optional(args, 3)) ? nil : try bounded(args[3]) | |
| 181 | 189 | let current = try absolute(try date()) |
| 182 | 190 | guard dayname == Days.weekday(current) else { return .nil } |
| 183 | 191 | let d = Days.date(current) |
| 184 | let limit = nthNamedAbsday(-n, dayname, d.month, d.year, d.day) | |
| 192 | let limit = try nthNamedAbsday(-n, dayname, d.month, d.year, d.day) | |
| 185 | 193 | let lastAbs = n > 0 ? limit : limit + 6 |
| 186 | 194 | let firstAbs = n > 0 ? limit - 6 : limit |
| 187 | 195 | let first = Days.date(firstAbs) |
| @@ -194,25 +202,26 @@ public enum DiarySexp { | ||
| 194 | 202 | default: false |
| 195 | 203 | } |
| 196 | 204 | } |
| 197 | func base(_ m: Int, _ y: Int) -> Int { day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) } | |
| 205 | func base(_ m: Int, _ y: Int) throws -> Int { try day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) } | |
| 198 | 206 | let applies: Bool |
| 199 | 207 | if first.month == last.month { |
| 200 | let b = base(first.month, first.year) | |
| 208 | let b = try base(first.month, first.year) | |
| 201 | 209 | applies = monthMatches(first.month) && first.day <= b && b <= last.day |
| 202 | 210 | } else { |
| 211 | let firstBase = try base(first.month, first.year) | |
| 212 | let lastBase = try base(last.month, last.year) | |
| 203 | 213 | 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)) | |
| 214 | && ((monthMatches(first.month) && first.day <= firstBase) || (monthMatches(last.month) && lastBase <= last.day)) | |
| 206 | 215 | } |
| 207 | 216 | return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil |
| 208 | 217 | } |
| 209 | 218 | lisp.define("diary-anniversary") { lisp, args in |
| 210 | 219 | try arity(args, 2...4, "diary-anniversary") |
| 211 | 220 | 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]) | |
| 221 | var dd = try bounded(made[1]) | |
| 222 | var mm = try bounded(made[0]) | |
| 214 | 223 | let y = try parts(try date()).year |
| 215 | let diff = Elisp.isNil(made[2]) ? 100 : y - (try Elisp.int(made[2])) | |
| 224 | let diff = Elisp.isNil(made[2]) ? 100 : y - (try bounded(made[2])) | |
| 216 | 225 | if mm == 2, dd == 29, !isLeap(y) { |
| 217 | 226 | mm = 3 |
| 218 | 227 | dd = 1 |
| @@ -233,12 +242,15 @@ public enum DiarySexp { | ||
| 233 | 242 | lisp.define("diary-remind") { lisp, args in |
| 234 | 243 | try arity(args, 2...3, "diary-remind") |
| 235 | 244 | var days = args[1] |
| 236 | if case .integer(let n) = days, n < 0 { days = Elisp.list((1...(-n)).map { .integer($0) }) } | |
| 245 | if case .integer(let n) = days, n < 0 { | |
| 246 | guard n >= -Elisp.lengthLimit else { throw Elisp.Signal(message: "Too many days for diary-remind") } | |
| 247 | days = Elisp.list((1...(-n)).map { .integer($0) }) | |
| 248 | } | |
| 237 | 249 | func remind(_ days: Sexp) throws -> Sexp { |
| 238 | 250 | let applies = try lisp.eval(args[0]) |
| 239 | 251 | if !Elisp.isNil(applies) { return applies } |
| 240 | 252 | if case .integer(let n) = days { |
| 241 | let shifted = gregorian(try absolute(try date()) + n) | |
| 253 | let shifted = gregorian(try absolute(try date()) + (try bounded(.integer(n)))) | |
| 242 | 254 | var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) } |
| 243 | 255 | guard !Elisp.isNil(found) else { return .nil } |
| 244 | 256 | if case .dotted = found { found = try Elisp.cdr(found) } else if case .list = found { found = try Elisp.cdr(found) } |
| @@ -258,7 +270,7 @@ public enum DiarySexp { | ||
| 258 | 270 | lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } } |
| 259 | 271 | lisp.define("org-class") { _, args in |
| 260 | 272 | try arity(args, 7...Int.max, "org-class") |
| 261 | let ints = try args.prefix(7).map(Elisp.int) | |
| 273 | let ints = try args.prefix(7).map(bounded) | |
| 262 | 274 | let first = Days.absolute(year: ints[0], month: ints[1], day: ints[2]) |
| 263 | 275 | let last = Days.absolute(year: ints[3], month: ints[4], day: ints[5]) |
| 264 | 276 | let d = try absolute(try date()) |
Sources/OrgCore/Capture/Capture.swift +4 −2
| @@ -289,7 +289,7 @@ public enum Capture { | ||
| 289 | 289 | let ns = text as NSString |
| 290 | 290 | var out = text |
| 291 | 291 | for m in regex.matches(in: text, range: NSRange(location: 0, length: ns.length)).reversed() where !escaped(ns, at: m.range.location) { |
| 292 | let n = Int(ns.substring(with: m.range(at: 1)))! | |
| 292 | let n = Int(ns.substring(with: m.range(at: 1))) ?? Int.max | |
| 293 | 293 | out = (out as NSString).replacingCharacters(in: m.range, with: n <= values.count ? values[n - 1] : "") |
| 294 | 294 | } |
| 295 | 295 | text = out |
| @@ -774,7 +774,9 @@ public enum Capture { | ||
| 774 | 774 | guard let m = pos.firstMatch(of: /(I+)([-+][0-9]+)/) else { throw Failure.message("Invalid table line specification \"\(pos)\"") } |
| 775 | 775 | let hlines = rows.indices.filter { rows[$0].range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil } |
| 776 | 776 | guard m.1.count <= hlines.count else { throw Failure.message("Invalid table line specification \"\(pos)\"") } |
| 777 | let delta = Int(m.2.replacingOccurrences(of: "+", with: ""))! | |
| 777 | guard let delta = Int(m.2.replacingOccurrences(of: "+", with: "")), (-999_999...999_999).contains(delta) else { | |
| 778 | throw Failure.message("Invalid table line specification \"\(pos)\"") | |
| 779 | } | |
| 778 | 780 | buffer.point = lineStart(hlines[m.1.count - 1] + delta + (delta < 0 ? 0 : -1) + 1) |
| 779 | 781 | } else if options.prepend { |
| 780 | 782 | if let rule = rows.firstIndex(where: { $0.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil }) { |
Sources/OrgCore/Compute/TableFormulas.swift +41 −25
| @@ -343,18 +343,28 @@ extension EmacsBuffer { | ||
| 343 | 343 | } |
| 344 | 344 | } |
| 345 | 345 | |
| 346 | /// A number written in a formula, small enough to work with. | |
| 347 | static func integer(_ s: String) throws -> Int { | |
| 348 | guard let n = Int(s.hasPrefix("+") ? String(s.dropFirst()) : s), (-999_999_999...999_999_999).contains(n) else { throw Calc.Unsupported("number size \(s)") } | |
| 349 | return n | |
| 350 | } | |
| 351 | ||
| 346 | 352 | /// `org-table-time-string-to-seconds`. |
| 347 | static func timeStringToSeconds(_ s: String) -> String { | |
| 353 | static func timeStringToSeconds(_ s: String) throws -> String { | |
| 348 | 354 | guard !s.isEmpty else { return s } |
| 349 | func seconds(_ sign: Substring, _ parts: [Substring]) -> String { | |
| 350 | let total = parts.enumerated().reduce(0) { sum, part in | |
| 351 | sum + (Int(part.element) ?? 0) * [3600, 60, 1][part.offset] | |
| 355 | func seconds(_ sign: Substring, _ parts: [Substring]) throws -> String { | |
| 356 | var total = 0 | |
| 357 | for (i, part) in parts.enumerated() { | |
| 358 | let (scaled, o1) = (Int(part) ?? Int.max).multipliedReportingOverflow(by: [3600, 60, 1][i]) | |
| 359 | let (sum, o2) = total.addingReportingOverflow(scaled) | |
| 360 | if o1 || o2 || Int(part) == nil { throw Calc.Unsupported("duration size") } | |
| 361 | total = sum | |
| 352 | 362 | } |
| 353 | 363 | return String(sign.isEmpty ? total : -total) |
| 354 | 364 | } |
| 355 | if let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+):([0-9]+)/) { return seconds(m.1, [m.2, m.3, m.4]) } | |
| 365 | if let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+):([0-9]+)/) { return try seconds(m.1, [m.2, m.3, m.4]) } | |
| 356 | 366 | if s.range(of: timestampBoth, options: .regularExpression) == nil, let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+)/) { |
| 357 | return seconds(m.1, [m.2, m.3]) | |
| 367 | return try seconds(m.1, [m.2, m.3]) | |
| 358 | 368 | } |
| 359 | 369 | return numberText(s) |
| 360 | 370 | } |
| @@ -363,9 +373,10 @@ extension EmacsBuffer { | ||
| 363 | 373 | |
| 364 | 374 | /// `org-table-time-seconds-to-string`, with `org-table-duration-custom-format` `hours` |
| 365 | 375 | /// and zero-padded hours. |
| 366 | static func secondsToString(_ seconds: Sexp, _ format: DurationFormat) -> String { | |
| 376 | static func secondsToString(_ seconds: Sexp, _ format: DurationFormat) throws -> String { | |
| 367 | 377 | let value = (try? Elisp.number(seconds))?.double ?? 0 |
| 368 | 378 | let magnitude = abs(value) |
| 379 | guard magnitude.isFinite, magnitude < 1e15 else { throw Calc.Unsupported("duration size") } | |
| 369 | 380 | let result: String |
| 370 | 381 | switch format { |
| 371 | 382 | case .hours: |
| @@ -406,7 +417,7 @@ extension EmacsBuffer { | ||
| 406 | 417 | |
| 407 | 418 | /// `org-table-formula-substitute-names`: column names become `$N`; parameters, named |
| 408 | 419 | /// fields and constants their values, in parentheses outside Lisp formulas. |
| 409 | func substituteNames(_ formula: String, analysis: TableAnalysis) -> String { | |
| 420 | func substituteNames(_ formula: String, analysis: TableAnalysis) throws -> String { | |
| 410 | 421 | let parenthesize = !formula.hasPrefix("'") |
| 411 | 422 | let duration = formula.range(of: ";.*[Tt].*$", options: .regularExpression) != nil |
| 412 | 423 | var new = formula |
| @@ -423,15 +434,19 @@ extension EmacsBuffer { | ||
| 423 | 434 | } |
| 424 | 435 | let regex = try! NSRegularExpression(pattern: "\\$([a-zA-Z][_a-zA-Z0-9]*)|(\\bremote\\([^)]*\\))") |
| 425 | 436 | var start = 0 |
| 437 | var substitutions = 0 | |
| 426 | 438 | while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) { |
| 427 | 439 | let ns = new as NSString |
| 440 | // A constant whose value names itself would go on forever, as it does in Org. | |
| 441 | substitutions += 1 | |
| 442 | if substitutions > 1000 { throw Calc.Unsupported("constants that refer to themselves") } | |
| 428 | 443 | if m.range(at: 2).location != NSNotFound { |
| 429 | 444 | start = NSMaxRange(m.range(at: 2)) |
| 430 | 445 | continue |
| 431 | 446 | } |
| 432 | 447 | start = m.range.location + 1 |
| 433 | 448 | var value = tableConstant(ns.substring(with: m.range(at: 1)), analysis: analysis) |
| 434 | if duration, value.contains(where: { !$0.isWhitespace }) { value = Self.timeStringToSeconds(value) } | |
| 449 | if duration, value.contains(where: { !$0.isWhitespace }) { value = try Self.timeStringToSeconds(value) } | |
| 435 | 450 | new = ns.replacingCharacters(in: m.range, with: parenthesize ? "(" + value + ")" : value) |
| 436 | 451 | } |
| 437 | 452 | return new |
| @@ -453,7 +468,7 @@ extension EmacsBuffer { | ||
| 453 | 468 | } |
| 454 | 469 | let remote = EmacsBuffer(string, point: m.range.location, settings: settings, options: options) |
| 455 | 470 | let analysis = try remote.tableAnalyze() |
| 456 | let resolved = remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis) | |
| 471 | let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis) | |
| 457 | 472 | let range = try! NSRegularExpression(pattern: Self.rangePattern) |
| 458 | 473 | let rns = resolved as NSString |
| 459 | 474 | if let r = range.firstMatch(in: resolved, range: NSRange(location: 0, length: rns.length)), r.range.length > 1 { |
| @@ -477,7 +492,7 @@ extension EmacsBuffer { | ||
| 477 | 492 | let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)") |
| 478 | 493 | while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) { |
| 479 | 494 | let ns = flags as NSString |
| 480 | let n = Int(ns.substring(with: m.range(at: 2)))! | |
| 495 | let n = try Self.integer(ns.substring(with: m.range(at: 2))) | |
| 481 | 496 | switch ns.substring(with: m.range(at: 1)) { |
| 482 | 497 | case "p": modes.precision = n |
| 483 | 498 | case "n": modes.format = .float(n) |
| @@ -505,16 +520,16 @@ extension EmacsBuffer { | ||
| 505 | 520 | } |
| 506 | 521 | if flags.contains(where: { !$0.isWhitespace }) { fmt = flags } |
| 507 | 522 | } |
| 508 | if !substituted { formula = try Self.handleFirstLast(substituteNames(formula, analysis: analysis), analysis: analysis) } | |
| 523 | if !substituted { formula = try Self.handleFirstLast(try substituteNames(formula, analysis: analysis), analysis: analysis) } | |
| 509 | 524 | var fields = Self.splitFields(substring(lineStart(point), lineEnd(point)).trimmingCharacters(in: .whitespaces), separator: " *\\| *") |
| 510 | if duration != nil { fields = fields.map(Self.timeStringToSeconds) } | |
| 525 | if duration != nil { fields = try fields.map(Self.timeStringToSeconds) } | |
| 511 | 526 | if mode.numbers { fields = fields.map { $0.contains(where: { !$0.isWhitespace }) ? Self.numberText($0) : $0 } } |
| 512 | 527 | if formula.count > 2, formula.hasPrefix("'(") { mode.lisp = literal ? .literal : .lisp } |
| 513 | 528 | func reference(_ value: TableRangeValue) throws -> String { |
| 514 | 529 | guard duration != nil else { return try Self.makeReference(value, mode) } |
| 515 | 530 | switch value { |
| 516 | case .field(let s): return try Self.makeReference(.field(Self.timeStringToSeconds(s)), mode) | |
| 517 | case .list(let items): return try Self.makeReference(.list(items.map(Self.timeStringToSeconds)), mode) | |
| 531 | case .field(let s): return try Self.makeReference(.field(try Self.timeStringToSeconds(s)), mode) | |
| 532 | case .list(let items): return try Self.makeReference(.list(try items.map(Self.timeStringToSeconds)), mode) | |
| 518 | 533 | } |
| 519 | 534 | } |
| 520 | 535 | let n0 = tableCurrentColumn() |
| @@ -561,15 +576,15 @@ extension EmacsBuffer { | ||
| 561 | 576 | let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)") |
| 562 | 577 | while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { |
| 563 | 578 | let ns = form as NSString |
| 564 | let a = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0) | |
| 565 | let b = Int(ns.substring(with: m.range(at: 3)))! + (m.range(at: 4).location != NSNotFound ? n0 : 0) | |
| 579 | let a = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0) | |
| 580 | let b = try Self.integer(ns.substring(with: m.range(at: 3))) + (m.range(at: 4).location != NSNotFound ? n0 : 0) | |
| 566 | 581 | guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") } |
| 567 | 582 | form = ns.replacingCharacters(in: m.range, with: try Self.makeReference(.list(Array(fields[(a - 1)..<b])), mode)) |
| 568 | 583 | } |
| 569 | 584 | let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)") |
| 570 | 585 | while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { |
| 571 | 586 | let ns = form as NSString |
| 572 | let n = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0) | |
| 587 | let n = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0) | |
| 573 | 588 | let index = (n == 0 ? n0 : max(n, 1)) - 1 |
| 574 | 589 | guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") } |
| 575 | 590 | let replacement = try Self.makeReference(.field(fields[index]), mode) |
| @@ -593,8 +608,8 @@ extension EmacsBuffer { | ||
| 593 | 608 | } |
| 594 | 609 | } |
| 595 | 610 | if let duration, !result.isEmpty, result != "#ERROR" { |
| 596 | let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? Self.timeStringToSeconds(result) : result | |
| 597 | result = Self.secondsToString(Elisp.stringToNumber(seconds), duration) | |
| 611 | let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? try Self.timeStringToSeconds(result) : result | |
| 612 | result = try Self.secondsToString(Elisp.stringToNumber(seconds), duration) | |
| 598 | 613 | } |
| 599 | 614 | } |
| 600 | 615 | if let fmt { |
| @@ -622,7 +637,7 @@ extension EmacsBuffer { | ||
| 622 | 637 | return "#ERROR" |
| 623 | 638 | } |
| 624 | 639 | var result = Elisp.printed(value) |
| 625 | if let duration { result = Self.secondsToString(Elisp.stringToNumber(result), duration) } | |
| 640 | if let duration { result = try Self.secondsToString(Elisp.stringToNumber(result), duration) } | |
| 626 | 641 | return result |
| 627 | 642 | } |
| 628 | 643 | |
| @@ -710,11 +725,11 @@ extension EmacsBuffer { | ||
| 710 | 725 | var columnEquations: [(Int, String)] = [] |
| 711 | 726 | var fieldEquations: [(String, String)] = [] |
| 712 | 727 | for (oldLHS, rawRHS) in stored { |
| 713 | let rhs = substituteNames(try Self.handleFirstLast(rawRHS, analysis: analysis), analysis: analysis) | |
| 728 | let rhs = try substituteNames(try Self.handleFirstLast(rawRHS, analysis: analysis), analysis: analysis) | |
| 714 | 729 | if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") } |
| 715 | 730 | let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis) |
| 716 | 731 | if lhs.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil { |
| 717 | columnEquations.append((Int(lhs.dropFirst())!, rhs)) | |
| 732 | columnEquations.append((try Self.integer(String(lhs.dropFirst())), rhs)) | |
| 718 | 733 | } else { |
| 719 | 734 | fieldEquations.append((lhs, rhs)) |
| 720 | 735 | } |
| @@ -723,7 +738,7 @@ extension EmacsBuffer { | ||
| 723 | 738 | var fields: [(row: Int, column: Int, rhs: String)] = [] |
| 724 | 739 | for (lhs, rhs) in fieldEquations { |
| 725 | 740 | if let m = lhs.firstMatch(of: /^@([0-9]+)\$([0-9]+)$/) { |
| 726 | fields.append((Int(m.1)!, Int(m.2)!, rhs)) | |
| 741 | fields.append((try Self.integer(String(m.1)), try Self.integer(String(m.2)), rhs)) | |
| 727 | 742 | } else if lhs.range(of: "^[a-zA-Z][_a-zA-Z0-9]*$", options: .regularExpression) != nil { |
| 728 | 743 | guard let location = analysis.namedFields.last(where: { $0.name == lhs }), let row = Self.lineToDline(location.line, analysis) else { |
| 729 | 744 | throw UserError(message: "Unknown field: \(lhs)") |
| @@ -732,7 +747,8 @@ extension EmacsBuffer { | ||
| 732 | 747 | } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil { |
| 733 | 748 | throw Calc.Unsupported("relative field formula") |
| 734 | 749 | } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) { |
| 735 | for c in 1...analysis.ncol { fields.append((Int(m.1)!, c, rhs)) } | |
| 750 | let row = try Self.integer(String(m.1)) | |
| 751 | for c in 1...analysis.ncol { fields.append((row, c, rhs)) } | |
| 736 | 752 | } else { |
| 737 | 753 | let corners = try rangeCorners(lhs, analysis: analysis) |
| 738 | 754 | guard let r1 = Self.lineToDline(corners.r1, analysis), let r2 = Self.lineToDline(corners.r2, analysis, above: true), r1 <= r2 else { |
Sources/OrgCore/Config/Elisp.swift +58 −16
| @@ -20,6 +20,11 @@ public final class Elisp { | ||
| 20 | 20 | private var functions: [String: Builtin] = [:] |
| 21 | 21 | private var steps = 0 |
| 22 | 22 | static let stepLimit = 1_000_000 |
| 23 | /// Nested evaluations, as `max-lisp-eval-depth` limits them. | |
| 24 | private var depth = 0 | |
| 25 | static let depthLimit = 1600 | |
| 26 | /// The most elements a list built at once may have. | |
| 27 | static let lengthLimit = 1_000_000 | |
| 23 | 28 | /// What `princ` and the other printers wrote: the innermost `with-output-to-string`, or |
| 24 | 29 | /// standard output. |
| 25 | 30 | private var outputs: [String] = [""] |
| @@ -168,9 +173,21 @@ public final class Elisp { | ||
| 168 | 173 | |
| 169 | 174 | // MARK: - Evaluation |
| 170 | 175 | |
| 176 | /// Less than 128 KB of this thread's stack left: background threads have only 512 KB. | |
| 177 | static func stackIsLow() -> Bool { | |
| 178 | var marker = 0 | |
| 179 | let here = withUnsafeMutablePointer(to: &marker) { UInt(bitPattern: $0) } | |
| 180 | let top = UInt(bitPattern: pthread_get_stackaddr_np(pthread_self())) | |
| 181 | let size = UInt(pthread_get_stacksize_np(pthread_self())) | |
| 182 | return here < top - size + 128 * 1024 | |
| 183 | } | |
| 184 | ||
| 171 | 185 | public func eval(_ form: Sexp) throws -> Sexp { |
| 172 | 186 | steps += 1 |
| 173 | 187 | if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") } |
| 188 | depth += 1 | |
| 189 | defer { depth -= 1 } | |
| 190 | if depth > Self.depthLimit || Self.stackIsLow() { throw Signal(message: "Lisp nesting exceeds ‘max-lisp-eval-depth’") } | |
| 174 | 191 | switch form { |
| 175 | 192 | case .symbol(let name): |
| 176 | 193 | if name == "nil" || name == "t" || name.hasPrefix(":") { return form } |
| @@ -285,15 +302,18 @@ public final class Elisp { | ||
| 285 | 302 | let spec = try args.first.map(Self.elements) ?? [] |
| 286 | 303 | guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") } |
| 287 | 304 | let source = try eval(spec[1]) |
| 288 | let items = name == "dolist" ? try Self.elements(source) : (0..<max(0, try Self.int(source))).map { Sexp.integer($0) } | |
| 305 | let items = name == "dolist" ? try Self.elements(source) : [] | |
| 306 | let count = name == "dolist" ? items.count : max(0, try Self.int(source)) | |
| 289 | 307 | scopes.append([:]) |
| 290 | 308 | defer { scopes.removeLast() } |
| 291 | for item in items { | |
| 292 | scopes[scopes.count - 1][variable] = item | |
| 309 | for i in 0..<count { | |
| 310 | steps += 1 | |
| 311 | if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") } | |
| 312 | scopes[scopes.count - 1][variable] = name == "dolist" ? items[i] : .integer(i) | |
| 293 | 313 | _ = try progn(args.dropFirst()) |
| 294 | 314 | } |
| 295 | 315 | if spec.count > 2 { |
| 296 | scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(items.count) : .nil | |
| 316 | scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(count) : .nil | |
| 297 | 317 | return try eval(spec[2]) |
| 298 | 318 | } |
| 299 | 319 | return .nil |
| @@ -379,7 +399,9 @@ public final class Elisp { | ||
| 379 | 399 | guard var result = numbers.first else { return .integer(identity) } |
| 380 | 400 | if numbers.count == 1, name == "-" { |
| 381 | 401 | switch result { |
| 382 | case .int(let i): return .integer(-i) | |
| 402 | case .int(let i): | |
| 403 | guard i != .min else { throw Unsupported(what: "integer size") } | |
| 404 | return .integer(-i) | |
| 383 | 405 | case .float(let d): return .float(-d) |
| 384 | 406 | } |
| 385 | 407 | } |
| @@ -412,14 +434,17 @@ public final class Elisp { | ||
| 412 | 434 | try arity(args, 2...2, "%") |
| 413 | 435 | let b = try int(args[1]) |
| 414 | 436 | guard b != 0 else { throw Signal(message: "Arithmetic error") } |
| 415 | return .integer(try int(args[0]) % b) | |
| 437 | let (r, overflow) = try int(args[0]).remainderReportingOverflow(dividingBy: b) | |
| 438 | if overflow { throw Unsupported(what: "integer size") } | |
| 439 | return .integer(r) | |
| 416 | 440 | } |
| 417 | 441 | lisp.define("mod") { _, args in |
| 418 | 442 | try arity(args, 2...2, "mod") |
| 419 | 443 | switch (try number(args[0]), try number(args[1])) { |
| 420 | 444 | case (.int(let a), .int(let b)): |
| 421 | 445 | guard b != 0 else { throw Signal(message: "Arithmetic error") } |
| 422 | let r = a % b | |
| 446 | let (r, overflow) = a.remainderReportingOverflow(dividingBy: b) | |
| 447 | if overflow { throw Unsupported(what: "integer size") } | |
| 423 | 448 | return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r) |
| 424 | 449 | case (let a, let b): |
| 425 | 450 | let r = fmod(a.double, b.double) |
| @@ -431,7 +456,9 @@ public final class Elisp { | ||
| 431 | 456 | lisp.define("abs") { _, args in |
| 432 | 457 | try arity(args, 1...1, "abs") |
| 433 | 458 | switch try number(args[0]) { |
| 434 | case .int(let i): return .integer(abs(i)) | |
| 459 | case .int(let i): | |
| 460 | guard i != .min else { throw Unsupported(what: "integer size") } | |
| 461 | return .integer(abs(i)) | |
| 435 | 462 | case .float(let d): return .float(abs(d)) |
| 436 | 463 | } |
| 437 | 464 | } |
| @@ -575,6 +602,8 @@ public final class Elisp { | ||
| 575 | 602 | let to = try int(args[1]) |
| 576 | 603 | let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1 |
| 577 | 604 | guard step != 0 else { throw Signal(message: "The increment can not be zero") } |
| 605 | let span = Double(to) - Double(from) | |
| 606 | guard span / Double(step) < Double(Self.lengthLimit) else { throw Signal(message: "Too many elements for number-sequence") } | |
| 578 | 607 | return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) }) |
| 579 | 608 | } |
| 580 | 609 | for name in ["memq", "member", "memql"] { |
| @@ -682,12 +711,12 @@ public final class Elisp { | ||
| 682 | 711 | lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) } |
| 683 | 712 | lisp.define("upcase") { _, args in |
| 684 | 713 | try arity(args, 1...1, "upcase") |
| 685 | if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).uppercased().unicodeScalars.first!.value } ?? UInt32(c))) } | |
| 714 | if case .integer(let c) = args[0] { return .integer(Int(String(try scalar(c)).uppercased().unicodeScalars.first!.value)) } | |
| 686 | 715 | return .string(try string(args[0]).uppercased()) |
| 687 | 716 | } |
| 688 | 717 | lisp.define("downcase") { _, args in |
| 689 | 718 | try arity(args, 1...1, "downcase") |
| 690 | if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).lowercased().unicodeScalars.first!.value } ?? UInt32(c))) } | |
| 719 | if case .integer(let c) = args[0] { return .integer(Int(String(try scalar(c)).lowercased().unicodeScalars.first!.value)) } | |
| 691 | 720 | return .string(try string(args[0]).lowercased()) |
| 692 | 721 | } |
| 693 | 722 | lisp.define("capitalize") { _, args in |
| @@ -753,17 +782,30 @@ public final class Elisp { | ||
| 753 | 782 | } |
| 754 | 783 | } |
| 755 | 784 | |
| 756 | static func add(_ n: Number, _ k: Int) -> Number { | |
| 785 | static func add(_ n: Number, _ k: Int) throws -> Number { | |
| 757 | 786 | switch n { |
| 758 | case .int(let i): .int(i + k) | |
| 759 | case .float(let d): .float(d + Double(k)) | |
| 787 | case .int(let i): | |
| 788 | let (sum, overflow) = i.addingReportingOverflow(k) | |
| 789 | if overflow { throw Unsupported(what: "integer size") } | |
| 790 | return .int(sum) | |
| 791 | case .float(let d): return .float(d + Double(k)) | |
| 792 | } | |
| 793 | } | |
| 794 | ||
| 795 | /// A character code as a character. | |
| 796 | static func scalar(_ code: Int) throws -> Unicode.Scalar { | |
| 797 | guard code >= 0, code <= 0x10FFFF, let scalar = Unicode.Scalar(UInt32(code)) else { | |
| 798 | throw Signal(message: "Invalid character: \(code)") | |
| 760 | 799 | } |
| 800 | return scalar | |
| 761 | 801 | } |
| 762 | 802 | |
| 763 | 803 | static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number { |
| 764 | 804 | if !floating, case .int(let x) = a, case .int(let y) = b { |
| 765 | 805 | guard y != 0 else { throw Signal(message: "Arithmetic error") } |
| 766 | return .int(x / y) | |
| 806 | let (q, overflow) = x.dividedReportingOverflow(by: y) | |
| 807 | if overflow { throw Unsupported(what: "integer size") } | |
| 808 | return .int(q) | |
| 767 | 809 | } |
| 768 | 810 | return .float(a.double / b.double) |
| 769 | 811 | } |
| @@ -807,7 +849,7 @@ public final class Elisp { | ||
| 807 | 849 | case .string(let s): return s |
| 808 | 850 | case _ where isNil(v): return "" |
| 809 | 851 | case .list, .vector: |
| 810 | return String(String.UnicodeScalarView(try sequence(v).map { Unicode.Scalar(UInt32(try int($0))) ?? " " })) | |
| 852 | return String(String.UnicodeScalarView(try sequence(v).map { try scalar(try int($0)) })) | |
| 811 | 853 | default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)") |
| 812 | 854 | } |
| 813 | 855 | } |
| @@ -887,7 +929,7 @@ public final class Elisp { | ||
| 887 | 929 | out += text |
| 888 | 930 | continue |
| 889 | 931 | case "c": |
| 890 | text = Unicode.Scalar(UInt32(try int(try argument()))).map { String($0) } ?? "" | |
| 932 | text = String(try scalar(try int(try argument()))) | |
| 891 | 933 | case "e", "f", "g": |
| 892 | 934 | let value = try number(try argument()).double |
| 893 | 935 | out += String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + String(conversion), value) |
Tests/OrgCoreTests/ElispTests.swift +48
| @@ -46,3 +46,51 @@ struct ElispTests { | ||
| 46 | 46 | } |
| 47 | 47 | } |
| 48 | 48 | } |
| 49 | ||
| 50 | /// Inputs that once crashed the evaluator, the diary functions or duration formulas. | |
| 51 | struct HostileInputTests { | |
| 52 | static let forms = [ | |
| 53 | "(1+ 9223372036854775807)", "(1- -9223372036854775808)", "(- -9223372036854775808)", "(abs -9223372036854775808)", | |
| 54 | "(% -9223372036854775808 -1)", "(mod -9223372036854775808 -1)", "(/ -9223372036854775808 -1)", | |
| 55 | "(upcase -1)", "(downcase -1)", "(concat (list -1))", "(format \"%c\" -1)", "(format \"%c\" 99999999)", | |
| 56 | "(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))", | |
| 57 | "(dotimes (i 10000000000) nil)", "(number-sequence 1 10000000000)", | |
| 58 | ] | |
| 59 | ||
| 60 | @Test func evaluatorRefusesWithoutCrashing() { | |
| 61 | for form in Self.forms { #expect(ElispTests.ours(form) == "error", "\(form)") } | |
| 62 | } | |
| 63 | ||
| 64 | /// On a stack the size of a background task's, deep recursion stops with an error. | |
| 65 | @Test func deepRecursionFitsABackgroundStack() async { | |
| 66 | let done = await withCheckedContinuation { continuation in | |
| 67 | let thread = Thread { | |
| 68 | continuation.resume(returning: ElispTests.ours("(funcall (lambda (f) (funcall f f)) (lambda (f) (funcall f f)))")) | |
| 69 | } | |
| 70 | thread.stackSize = 512 * 1024 | |
| 71 | thread.start() | |
| 72 | } | |
| 73 | #expect(done == "error") | |
| 74 | } | |
| 75 | ||
| 76 | @Test func diaryFunctionsRefuseWithoutCrashing() throws { | |
| 77 | let day = Days.absolute(year: 2026, month: 10, day: 7) | |
| 78 | for form in ["(calendar-last-day-of-month -20 2024)", "(calendar-gregorian-from-absolute 9223372036854775807)", | |
| 79 | "(calendar-nth-named-absday -9223372036854775808 1 10 2026)", "(diary-remind '(diary-date 1 1 2027) -9223372036854775808)", | |
| 80 | "(diary-float 10 3 -9223372036854775808)", "(diary-anniversary 10 7 -9223372036854775808)"] { | |
| 81 | #expect(DiarySexp.entries(try LispReader.readFirst(form).sexp, entry: "E", day: day) == nil, "\(form)") | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | @Test func durationsAndHugeNumbersDoNotCrashFormulas() { | |
| 86 | for table in [ | |
| 87 | "| 1e300 | |\n#+TBLFM: $2=$1*2;T\n", "| 1e300 | |\n#+TBLFM: $2='(* $1 2);T\n", "| 9999999999999999:00:00 | |\n#+TBLFM: $2=$1;T\n", | |
| 88 | "| 1 | |\n#+TBLFM: $2=$1;p99999999999999999999\n", "| 1 | |\n#+TBLFM: $2=$99999999999999999999\n", | |
| 89 | "| 1 | |\n#+TBLFM: @99999999999999999999$2=1\n", "#+CONSTANTS: a=$a\n| 1 | |\n#+TBLFM: $2=$a\n", | |
| 90 | ] { | |
| 91 | let caret = (table as NSString).range(of: "#+TBLFM").location + 3 | |
| 92 | let result = runCommand(TableRecalculate(all: true), table, caret: caret) | |
| 93 | #expect(result.text == table, "\(table)") | |
| 94 | } | |
| 95 | } | |
| 96 | } | |