Commit b4ec29e3b2
Verified · cmc
Layout: unified · split
Sources/OrgCore/Agenda/DiarySexp.swift +37 −25
| @@ -37,10 +37,17 @@ public enum DiarySexp { | |||
| 37 | return .list([.integer(date.month), .integer(date.day), .integer(date.year)]) | 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 | static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) { | 47 | static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) { |
| 41 | let items = try Elisp.elements(date) | 48 | let items = try Elisp.elements(date) |
| 42 | guard items.count == 3 else { throw Elisp.Signal(message: "Bad date \(date.description)") } | 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 | static func absolute(_ date: Sexp) throws -> Int { | 53 | static func absolute(_ date: Sexp) throws -> Int { |
| @@ -50,17 +57,18 @@ public enum DiarySexp { | |||
| 50 | 57 | ||
| 51 | static func isLeap(_ year: Int) -> Bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) } | 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 { | 60 | static func lastDay(month: Int, year: Int) throws -> Int { |
| 54 | month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(month - 1 + 12) % 12] | 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 | /// `calendar-nth-named-absday`. | 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 | func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 } | 67 | func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 } |
| 60 | if n > 0 { | 68 | if n > 0 { |
| 61 | return 7 * (n - 1) + onOrBefore(6 + Days.absolute(year: year, month: month, day: day ?? 1)) | 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 | /// `diary-ordinal-suffix`. | 74 | /// `diary-ordinal-suffix`. |
| @@ -102,7 +110,7 @@ public enum DiarySexp { | |||
| 102 | } | 110 | } |
| 103 | lisp.define("calendar-gregorian-from-absolute") { _, args in | 111 | lisp.define("calendar-gregorian-from-absolute") { _, args in |
| 104 | try arity(args, 1...1, "calendar-gregorian-from-absolute") | 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 | lisp.define("calendar-day-of-week") { _, args in | 115 | lisp.define("calendar-day-of-week") { _, args in |
| 108 | try arity(args, 1...1, "calendar-day-of-week") | 116 | try arity(args, 1...1, "calendar-day-of-week") |
| @@ -114,7 +122,7 @@ public enum DiarySexp { | |||
| 114 | } | 122 | } |
| 115 | lisp.define("calendar-last-day-of-month") { _, args in | 123 | lisp.define("calendar-last-day-of-month") { _, args in |
| 116 | try arity(args, 2...2, "calendar-last-day-of-month") | 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 | lisp.define("calendar-date-equal") { _, args in | 127 | lisp.define("calendar-date-equal") { _, args in |
| 120 | try arity(args, 2...2, "calendar-date-equal") | 128 | try arity(args, 2...2, "calendar-date-equal") |
| @@ -129,15 +137,15 @@ public enum DiarySexp { | |||
| 129 | } | 137 | } |
| 130 | lisp.define("calendar-nth-named-absday") { _, args in | 138 | lisp.define("calendar-nth-named-absday") { _, args in |
| 131 | try arity(args, 4...5, "calendar-nth-named-absday") | 139 | try arity(args, 4...5, "calendar-nth-named-absday") |
| 132 | let day = Elisp.isNil(optional(args, 4)) ? nil : try Elisp.int(args[4]) | 140 | let day = Elisp.isNil(optional(args, 4)) ? nil : try bounded(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)) | 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 | lisp.define("calendar-nth-named-day") { lisp, args in | 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 | lisp.define("calendar-iso-from-absolute") { _, args in | 146 | lisp.define("calendar-iso-from-absolute") { _, args in |
| 139 | try arity(args, 1...1, "calendar-iso-from-absolute") | 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 | let thursday = day - (Days.weekday(day) + 6) % 7 + 3 | 149 | 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)]) | 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 | lisp.define("diary-float") { _, args in | 183 | lisp.define("diary-float") { _, args in |
| 176 | try arity(args, 3...5, "diary-float") | 184 | try arity(args, 3...5, "diary-float") |
| 177 | let month = args[0] | 185 | let month = args[0] |
| 178 | let dayname = try Elisp.int(args[1]) | 186 | let dayname = try bounded(args[1]) |
| 179 | let n = try Elisp.int(args[2]) | 187 | let n = try bounded(args[2]) |
| 180 | let day = Elisp.isNil(optional(args, 3)) ? nil : try Elisp.int(args[3]) | 188 | let day = Elisp.isNil(optional(args, 3)) ? nil : try bounded(args[3]) |
| 181 | let current = try absolute(try date()) | 189 | let current = try absolute(try date()) |
| 182 | guard dayname == Days.weekday(current) else { return .nil } | 190 | guard dayname == Days.weekday(current) else { return .nil } |
| 183 | let d = Days.date(current) | 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 | let lastAbs = n > 0 ? limit : limit + 6 | 193 | let lastAbs = n > 0 ? limit : limit + 6 |
| 186 | let firstAbs = n > 0 ? limit - 6 : limit | 194 | let firstAbs = n > 0 ? limit - 6 : limit |
| 187 | let first = Days.date(firstAbs) | 195 | let first = Days.date(firstAbs) |
| @@ -194,25 +202,26 @@ public enum DiarySexp { | |||
| 194 | default: false | 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 | let applies: Bool | 206 | let applies: Bool |
| 199 | if first.month == last.month { | 207 | if first.month == last.month { |
| 200 | let b = base(first.month, first.year) | 208 | let b = try base(first.month, first.year) |
| 201 | applies = monthMatches(first.month) && first.day <= b && b <= last.day | 209 | applies = monthMatches(first.month) && first.day <= b && b <= last.day |
| 202 | } else { | 210 | } else { |
| 211 | let firstBase = try base(first.month, first.year) | ||
| 212 | let lastBase = try base(last.month, last.year) | ||
| 203 | applies = (first.year < last.year || (first.year == last.year && first.month < last.month)) | 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)) | 214 | && ((monthMatches(first.month) && first.day <= firstBase) || (monthMatches(last.month) && lastBase <= last.day)) |
| 205 | || (monthMatches(last.month) && base(last.month, last.year) <= last.day)) | ||
| 206 | } | 215 | } |
| 207 | return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil | 216 | return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil |
| 208 | } | 217 | } |
| 209 | lisp.define("diary-anniversary") { lisp, args in | 218 | lisp.define("diary-anniversary") { lisp, args in |
| 210 | try arity(args, 2...4, "diary-anniversary") | 219 | try arity(args, 2...4, "diary-anniversary") |
| 211 | let made = try makeDate(lisp, args[0], args[1], optional(args, 2)) | 220 | let made = try makeDate(lisp, args[0], args[1], optional(args, 2)) |
| 212 | var dd = try Elisp.int(made[1]) | 221 | var dd = try bounded(made[1]) |
| 213 | var mm = try Elisp.int(made[0]) | 222 | var mm = try bounded(made[0]) |
| 214 | let y = try parts(try date()).year | 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 | if mm == 2, dd == 29, !isLeap(y) { | 225 | if mm == 2, dd == 29, !isLeap(y) { |
| 217 | mm = 3 | 226 | mm = 3 |
| 218 | dd = 1 | 227 | dd = 1 |
| @@ -233,12 +242,15 @@ public enum DiarySexp { | |||
| 233 | lisp.define("diary-remind") { lisp, args in | 242 | lisp.define("diary-remind") { lisp, args in |
| 234 | try arity(args, 2...3, "diary-remind") | 243 | try arity(args, 2...3, "diary-remind") |
| 235 | var days = args[1] | 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 | func remind(_ days: Sexp) throws -> Sexp { | 249 | func remind(_ days: Sexp) throws -> Sexp { |
| 238 | let applies = try lisp.eval(args[0]) | 250 | let applies = try lisp.eval(args[0]) |
| 239 | if !Elisp.isNil(applies) { return applies } | 251 | if !Elisp.isNil(applies) { return applies } |
| 240 | if case .integer(let n) = days { | 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 | var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) } | 254 | var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) } |
| 243 | guard !Elisp.isNil(found) else { return .nil } | 255 | 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) } | 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 | lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } } | 270 | lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } } |
| 259 | lisp.define("org-class") { _, args in | 271 | lisp.define("org-class") { _, args in |
| 260 | try arity(args, 7...Int.max, "org-class") | 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 | let first = Days.absolute(year: ints[0], month: ints[1], day: ints[2]) | 274 | 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]) | 275 | let last = Days.absolute(year: ints[3], month: ints[4], day: ints[5]) |
| 264 | let d = try absolute(try date()) | 276 | let d = try absolute(try date()) |
Sources/OrgCore/Capture/Capture.swift +4 −2
| @@ -289,7 +289,7 @@ public enum Capture { | |||
| 289 | let ns = text as NSString | 289 | let ns = text as NSString |
| 290 | var out = text | 290 | var out = text |
| 291 | for m in regex.matches(in: text, range: NSRange(location: 0, length: ns.length)).reversed() where !escaped(ns, at: m.range.location) { | 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 | out = (out as NSString).replacingCharacters(in: m.range, with: n <= values.count ? values[n - 1] : "") | 293 | out = (out as NSString).replacingCharacters(in: m.range, with: n <= values.count ? values[n - 1] : "") |
| 294 | } | 294 | } |
| 295 | text = out | 295 | text = out |
| @@ -774,7 +774,9 @@ public enum Capture { | |||
| 774 | guard let m = pos.firstMatch(of: /(I+)([-+][0-9]+)/) else { throw Failure.message("Invalid table line specification \"\(pos)\"") } | 774 | guard let m = pos.firstMatch(of: /(I+)([-+][0-9]+)/) else { throw Failure.message("Invalid table line specification \"\(pos)\"") } |
| 775 | let hlines = rows.indices.filter { rows[$0].range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil } | 775 | let hlines = rows.indices.filter { rows[$0].range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil } |
| 776 | guard m.1.count <= hlines.count else { throw Failure.message("Invalid table line specification \"\(pos)\"") } | 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 | buffer.point = lineStart(hlines[m.1.count - 1] + delta + (delta < 0 ? 0 : -1) + 1) | 780 | buffer.point = lineStart(hlines[m.1.count - 1] + delta + (delta < 0 ? 0 : -1) + 1) |
| 779 | } else if options.prepend { | 781 | } else if options.prepend { |
| 780 | if let rule = rows.firstIndex(where: { $0.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil }) { | 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 | /// `org-table-time-string-to-seconds`. | 352 | /// `org-table-time-string-to-seconds`. |
| 347 | static func timeStringToSeconds(_ s: String) -> String { | 353 | static func timeStringToSeconds(_ s: String) throws -> String { |
| 348 | guard !s.isEmpty else { return s } | 354 | guard !s.isEmpty else { return s } |
| 349 | func seconds(_ sign: Substring, _ parts: [Substring]) -> String { | 355 | func seconds(_ sign: Substring, _ parts: [Substring]) throws -> String { |
| 350 | let total = parts.enumerated().reduce(0) { sum, part in | 356 | var total = 0 |
| 351 | sum + (Int(part.element) ?? 0) * [3600, 60, 1][part.offset] | 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 | return String(sign.isEmpty ? total : -total) | 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 | if s.range(of: timestampBoth, options: .regularExpression) == nil, let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+)/) { | 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 | return numberText(s) | 369 | return numberText(s) |
| 360 | } | 370 | } |
| @@ -363,9 +373,10 @@ extension EmacsBuffer { | |||
| 363 | 373 | ||
| 364 | /// `org-table-time-seconds-to-string`, with `org-table-duration-custom-format` `hours` | 374 | /// `org-table-time-seconds-to-string`, with `org-table-duration-custom-format` `hours` |
| 365 | /// and zero-padded hours. | 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 | let value = (try? Elisp.number(seconds))?.double ?? 0 | 377 | let value = (try? Elisp.number(seconds))?.double ?? 0 |
| 368 | let magnitude = abs(value) | 378 | let magnitude = abs(value) |
| 379 | guard magnitude.isFinite, magnitude < 1e15 else { throw Calc.Unsupported("duration size") } | ||
| 369 | let result: String | 380 | let result: String |
| 370 | switch format { | 381 | switch format { |
| 371 | case .hours: | 382 | case .hours: |
| @@ -406,7 +417,7 @@ extension EmacsBuffer { | |||
| 406 | 417 | ||
| 407 | /// `org-table-formula-substitute-names`: column names become `$N`; parameters, named | 418 | /// `org-table-formula-substitute-names`: column names become `$N`; parameters, named |
| 408 | /// fields and constants their values, in parentheses outside Lisp formulas. | 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 | let parenthesize = !formula.hasPrefix("'") | 421 | let parenthesize = !formula.hasPrefix("'") |
| 411 | let duration = formula.range(of: ";.*[Tt].*$", options: .regularExpression) != nil | 422 | let duration = formula.range(of: ";.*[Tt].*$", options: .regularExpression) != nil |
| 412 | var new = formula | 423 | var new = formula |
| @@ -423,15 +434,19 @@ extension EmacsBuffer { | |||
| 423 | } | 434 | } |
| 424 | let regex = try! NSRegularExpression(pattern: "\\$([a-zA-Z][_a-zA-Z0-9]*)|(\\bremote\\([^)]*\\))") | 435 | let regex = try! NSRegularExpression(pattern: "\\$([a-zA-Z][_a-zA-Z0-9]*)|(\\bremote\\([^)]*\\))") |
| 425 | var start = 0 | 436 | var start = 0 |
| 437 | var substitutions = 0 | ||
| 426 | while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) { | 438 | while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) { |
| 427 | let ns = new as NSString | 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 | if m.range(at: 2).location != NSNotFound { | 443 | if m.range(at: 2).location != NSNotFound { |
| 429 | start = NSMaxRange(m.range(at: 2)) | 444 | start = NSMaxRange(m.range(at: 2)) |
| 430 | continue | 445 | continue |
| 431 | } | 446 | } |
| 432 | start = m.range.location + 1 | 447 | start = m.range.location + 1 |
| 433 | var value = tableConstant(ns.substring(with: m.range(at: 1)), analysis: analysis) | 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 | new = ns.replacingCharacters(in: m.range, with: parenthesize ? "(" + value + ")" : value) | 450 | new = ns.replacingCharacters(in: m.range, with: parenthesize ? "(" + value + ")" : value) |
| 436 | } | 451 | } |
| 437 | return new | 452 | return new |
| @@ -453,7 +468,7 @@ extension EmacsBuffer { | |||
| 453 | } | 468 | } |
| 454 | let remote = EmacsBuffer(string, point: m.range.location, settings: settings, options: options) | 469 | let remote = EmacsBuffer(string, point: m.range.location, settings: settings, options: options) |
| 455 | let analysis = try remote.tableAnalyze() | 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 | let range = try! NSRegularExpression(pattern: Self.rangePattern) | 472 | let range = try! NSRegularExpression(pattern: Self.rangePattern) |
| 458 | let rns = resolved as NSString | 473 | let rns = resolved as NSString |
| 459 | if let r = range.firstMatch(in: resolved, range: NSRange(location: 0, length: rns.length)), r.range.length > 1 { | 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 | let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)") | 492 | let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)") |
| 478 | while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) { | 493 | while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) { |
| 479 | let ns = flags as NSString | 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 | switch ns.substring(with: m.range(at: 1)) { | 496 | switch ns.substring(with: m.range(at: 1)) { |
| 482 | case "p": modes.precision = n | 497 | case "p": modes.precision = n |
| 483 | case "n": modes.format = .float(n) | 498 | case "n": modes.format = .float(n) |
| @@ -505,16 +520,16 @@ extension EmacsBuffer { | |||
| 505 | } | 520 | } |
| 506 | if flags.contains(where: { !$0.isWhitespace }) { fmt = flags } | 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 | var fields = Self.splitFields(substring(lineStart(point), lineEnd(point)).trimmingCharacters(in: .whitespaces), separator: " *\\| *") | 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 | if mode.numbers { fields = fields.map { $0.contains(where: { !$0.isWhitespace }) ? Self.numberText($0) : $0 } } | 526 | if mode.numbers { fields = fields.map { $0.contains(where: { !$0.isWhitespace }) ? Self.numberText($0) : $0 } } |
| 512 | if formula.count > 2, formula.hasPrefix("'(") { mode.lisp = literal ? .literal : .lisp } | 527 | if formula.count > 2, formula.hasPrefix("'(") { mode.lisp = literal ? .literal : .lisp } |
| 513 | func reference(_ value: TableRangeValue) throws -> String { | 528 | func reference(_ value: TableRangeValue) throws -> String { |
| 514 | guard duration != nil else { return try Self.makeReference(value, mode) } | 529 | guard duration != nil else { return try Self.makeReference(value, mode) } |
| 515 | switch value { | 530 | switch value { |
| 516 | case .field(let s): return try Self.makeReference(.field(Self.timeStringToSeconds(s)), mode) | 531 | case .field(let s): return try Self.makeReference(.field(try Self.timeStringToSeconds(s)), mode) |
| 517 | case .list(let items): return try Self.makeReference(.list(items.map(Self.timeStringToSeconds)), mode) | 532 | case .list(let items): return try Self.makeReference(.list(try items.map(Self.timeStringToSeconds)), mode) |
| 518 | } | 533 | } |
| 519 | } | 534 | } |
| 520 | let n0 = tableCurrentColumn() | 535 | let n0 = tableCurrentColumn() |
| @@ -561,15 +576,15 @@ extension EmacsBuffer { | |||
| 561 | let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)") | 576 | let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)") |
| 562 | while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { | 577 | while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { |
| 563 | let ns = form as NSString | 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) | 579 | let a = try Self.integer(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) | 580 | let b = try Self.integer(ns.substring(with: m.range(at: 3))) + (m.range(at: 4).location != NSNotFound ? n0 : 0) |
| 566 | guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") } | 581 | guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") } |
| 567 | form = ns.replacingCharacters(in: m.range, with: try Self.makeReference(.list(Array(fields[(a - 1)..<b])), mode)) | 582 | form = ns.replacingCharacters(in: m.range, with: try Self.makeReference(.list(Array(fields[(a - 1)..<b])), mode)) |
| 568 | } | 583 | } |
| 569 | let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)") | 584 | let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)") |
| 570 | while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { | 585 | while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) { |
| 571 | let ns = form as NSString | 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 | let index = (n == 0 ? n0 : max(n, 1)) - 1 | 588 | let index = (n == 0 ? n0 : max(n, 1)) - 1 |
| 574 | guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") } | 589 | guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") } |
| 575 | let replacement = try Self.makeReference(.field(fields[index]), mode) | 590 | let replacement = try Self.makeReference(.field(fields[index]), mode) |
| @@ -593,8 +608,8 @@ extension EmacsBuffer { | |||
| 593 | } | 608 | } |
| 594 | } | 609 | } |
| 595 | if let duration, !result.isEmpty, result != "#ERROR" { | 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 | 611 | let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? try Self.timeStringToSeconds(result) : result |
| 597 | result = Self.secondsToString(Elisp.stringToNumber(seconds), duration) | 612 | result = try Self.secondsToString(Elisp.stringToNumber(seconds), duration) |
| 598 | } | 613 | } |
| 599 | } | 614 | } |
| 600 | if let fmt { | 615 | if let fmt { |
| @@ -622,7 +637,7 @@ extension EmacsBuffer { | |||
| 622 | return "#ERROR" | 637 | return "#ERROR" |
| 623 | } | 638 | } |
| 624 | var result = Elisp.printed(value) | 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 | return result | 641 | return result |
| 627 | } | 642 | } |
| 628 | 643 | ||
| @@ -710,11 +725,11 @@ extension EmacsBuffer { | |||
| 710 | var columnEquations: [(Int, String)] = [] | 725 | var columnEquations: [(Int, String)] = [] |
| 711 | var fieldEquations: [(String, String)] = [] | 726 | var fieldEquations: [(String, String)] = [] |
| 712 | for (oldLHS, rawRHS) in stored { | 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 | if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") } | 729 | if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") } |
| 715 | let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis) | 730 | let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis) |
| 716 | if lhs.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil { | 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 | } else { | 733 | } else { |
| 719 | fieldEquations.append((lhs, rhs)) | 734 | fieldEquations.append((lhs, rhs)) |
| 720 | } | 735 | } |
| @@ -723,7 +738,7 @@ extension EmacsBuffer { | |||
| 723 | var fields: [(row: Int, column: Int, rhs: String)] = [] | 738 | var fields: [(row: Int, column: Int, rhs: String)] = [] |
| 724 | for (lhs, rhs) in fieldEquations { | 739 | for (lhs, rhs) in fieldEquations { |
| 725 | if let m = lhs.firstMatch(of: /^@([0-9]+)\$([0-9]+)$/) { | 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 | } else if lhs.range(of: "^[a-zA-Z][_a-zA-Z0-9]*$", options: .regularExpression) != nil { | 742 | } else if lhs.range(of: "^[a-zA-Z][_a-zA-Z0-9]*$", options: .regularExpression) != nil { |
| 728 | guard let location = analysis.namedFields.last(where: { $0.name == lhs }), let row = Self.lineToDline(location.line, analysis) else { | 743 | guard let location = analysis.namedFields.last(where: { $0.name == lhs }), let row = Self.lineToDline(location.line, analysis) else { |
| 729 | throw UserError(message: "Unknown field: \(lhs)") | 744 | throw UserError(message: "Unknown field: \(lhs)") |
| @@ -732,7 +747,8 @@ extension EmacsBuffer { | |||
| 732 | } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil { | 747 | } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil { |
| 733 | throw Calc.Unsupported("relative field formula") | 748 | throw Calc.Unsupported("relative field formula") |
| 734 | } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) { | 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 | } else { | 752 | } else { |
| 737 | let corners = try rangeCorners(lhs, analysis: analysis) | 753 | let corners = try rangeCorners(lhs, analysis: analysis) |
| 738 | guard let r1 = Self.lineToDline(corners.r1, analysis), let r2 = Self.lineToDline(corners.r2, analysis, above: true), r1 <= r2 else { | 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 | private var functions: [String: Builtin] = [:] | 20 | private var functions: [String: Builtin] = [:] |
| 21 | private var steps = 0 | 21 | private var steps = 0 |
| 22 | static let stepLimit = 1_000_000 | 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 | /// What `princ` and the other printers wrote: the innermost `with-output-to-string`, or | 28 | /// What `princ` and the other printers wrote: the innermost `with-output-to-string`, or |
| 24 | /// standard output. | 29 | /// standard output. |
| 25 | private var outputs: [String] = [""] | 30 | private var outputs: [String] = [""] |
| @@ -168,9 +173,21 @@ public final class Elisp { | |||
| 168 | 173 | ||
| 169 | // MARK: - Evaluation | 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 | public func eval(_ form: Sexp) throws -> Sexp { | 185 | public func eval(_ form: Sexp) throws -> Sexp { |
| 172 | steps += 1 | 186 | steps += 1 |
| 173 | if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") } | 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 | switch form { | 191 | switch form { |
| 175 | case .symbol(let name): | 192 | case .symbol(let name): |
| 176 | if name == "nil" || name == "t" || name.hasPrefix(":") { return form } | 193 | if name == "nil" || name == "t" || name.hasPrefix(":") { return form } |
| @@ -285,15 +302,18 @@ public final class Elisp { | |||
| 285 | let spec = try args.first.map(Self.elements) ?? [] | 302 | let spec = try args.first.map(Self.elements) ?? [] |
| 286 | guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") } | 303 | guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") } |
| 287 | let source = try eval(spec[1]) | 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 | scopes.append([:]) | 307 | scopes.append([:]) |
| 290 | defer { scopes.removeLast() } | 308 | defer { scopes.removeLast() } |
| 291 | for item in items { | 309 | for i in 0..<count { |
| 292 | scopes[scopes.count - 1][variable] = item | 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 | _ = try progn(args.dropFirst()) | 313 | _ = try progn(args.dropFirst()) |
| 294 | } | 314 | } |
| 295 | if spec.count > 2 { | 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 | return try eval(spec[2]) | 317 | return try eval(spec[2]) |
| 298 | } | 318 | } |
| 299 | return .nil | 319 | return .nil |
| @@ -379,7 +399,9 @@ public final class Elisp { | |||
| 379 | guard var result = numbers.first else { return .integer(identity) } | 399 | guard var result = numbers.first else { return .integer(identity) } |
| 380 | if numbers.count == 1, name == "-" { | 400 | if numbers.count == 1, name == "-" { |
| 381 | switch result { | 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 | case .float(let d): return .float(-d) | 405 | case .float(let d): return .float(-d) |
| 384 | } | 406 | } |
| 385 | } | 407 | } |
| @@ -412,14 +434,17 @@ public final class Elisp { | |||
| 412 | try arity(args, 2...2, "%") | 434 | try arity(args, 2...2, "%") |
| 413 | let b = try int(args[1]) | 435 | let b = try int(args[1]) |
| 414 | guard b != 0 else { throw Signal(message: "Arithmetic error") } | 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 | lisp.define("mod") { _, args in | 441 | lisp.define("mod") { _, args in |
| 418 | try arity(args, 2...2, "mod") | 442 | try arity(args, 2...2, "mod") |
| 419 | switch (try number(args[0]), try number(args[1])) { | 443 | switch (try number(args[0]), try number(args[1])) { |
| 420 | case (.int(let a), .int(let b)): | 444 | case (.int(let a), .int(let b)): |
| 421 | guard b != 0 else { throw Signal(message: "Arithmetic error") } | 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 | return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r) | 448 | return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r) |
| 424 | case (let a, let b): | 449 | case (let a, let b): |
| 425 | let r = fmod(a.double, b.double) | 450 | let r = fmod(a.double, b.double) |
| @@ -431,7 +456,9 @@ public final class Elisp { | |||
| 431 | lisp.define("abs") { _, args in | 456 | lisp.define("abs") { _, args in |
| 432 | try arity(args, 1...1, "abs") | 457 | try arity(args, 1...1, "abs") |
| 433 | switch try number(args[0]) { | 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 | case .float(let d): return .float(abs(d)) | 462 | case .float(let d): return .float(abs(d)) |
| 436 | } | 463 | } |
| 437 | } | 464 | } |
| @@ -575,6 +602,8 @@ public final class Elisp { | |||
| 575 | let to = try int(args[1]) | 602 | let to = try int(args[1]) |
| 576 | let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1 | 603 | let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1 |
| 577 | guard step != 0 else { throw Signal(message: "The increment can not be zero") } | 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 | return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) }) | 607 | return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) }) |
| 579 | } | 608 | } |
| 580 | for name in ["memq", "member", "memql"] { | 609 | for name in ["memq", "member", "memql"] { |
| @@ -682,12 +711,12 @@ public final class Elisp { | |||
| 682 | lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) } | 711 | lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) } |
| 683 | lisp.define("upcase") { _, args in | 712 | lisp.define("upcase") { _, args in |
| 684 | try arity(args, 1...1, "upcase") | 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 | return .string(try string(args[0]).uppercased()) | 715 | return .string(try string(args[0]).uppercased()) |
| 687 | } | 716 | } |
| 688 | lisp.define("downcase") { _, args in | 717 | lisp.define("downcase") { _, args in |
| 689 | try arity(args, 1...1, "downcase") | 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 | return .string(try string(args[0]).lowercased()) | 720 | return .string(try string(args[0]).lowercased()) |
| 692 | } | 721 | } |
| 693 | lisp.define("capitalize") { _, args in | 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 | switch n { | 786 | switch n { |
| 758 | case .int(let i): .int(i + k) | 787 | case .int(let i): |
| 759 | case .float(let d): .float(d + Double(k)) | 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 | static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number { | 803 | static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number { |
| 764 | if !floating, case .int(let x) = a, case .int(let y) = b { | 804 | if !floating, case .int(let x) = a, case .int(let y) = b { |
| 765 | guard y != 0 else { throw Signal(message: "Arithmetic error") } | 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 | return .float(a.double / b.double) | 810 | return .float(a.double / b.double) |
| 769 | } | 811 | } |
| @@ -807,7 +849,7 @@ public final class Elisp { | |||
| 807 | case .string(let s): return s | 849 | case .string(let s): return s |
| 808 | case _ where isNil(v): return "" | 850 | case _ where isNil(v): return "" |
| 809 | case .list, .vector: | 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 | default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)") | 853 | default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)") |
| 812 | } | 854 | } |
| 813 | } | 855 | } |
| @@ -887,7 +929,7 @@ public final class Elisp { | |||
| 887 | out += text | 929 | out += text |
| 888 | continue | 930 | continue |
| 889 | case "c": | 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 | case "e", "f", "g": | 933 | case "e", "f", "g": |
| 892 | let value = try number(try argument()).double | 934 | let value = try number(try argument()).double |
| 893 | out += String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + String(conversion), value) | 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 | } | ||