import CCalcPadEngine import Foundation /// Calculation engine backed by the Rust CalcPad engine via C FFI. /// /// Thread safety: Each call creates its own `EvalContext` on the Rust side, /// so concurrent calls from different threads are safe. The C FFI functions /// are documented as safe to call from any thread. final class RustCalculationEngine: CalculationEngine, @unchecked Sendable { private let decoder = JSONDecoder() func evaluateLine(_ line: String) -> LineResult { evaluateSingleLine(line, lineNumber: 1) } func evaluateSheet(_ text: String) -> [LineResult] { let lines = text.components(separatedBy: "\n") // Build C string array for the FFI call let cStrings = lines.map { strdup($0) } defer { cStrings.forEach { free($0) } } var pointers = cStrings.map { UnsafePointer($0) } let resultPtr = pointers.withUnsafeMutableBufferPointer { buffer -> UnsafeMutablePointer? in calcpad_eval_sheet(buffer.baseAddress, Int32(lines.count)) } guard let resultPtr else { return lines.enumerated().map { index, line in LineResult(id: index + 1, expression: line, result: "Error", isError: true) } } defer { calcpad_free_result(resultPtr) } let jsonString = String(cString: resultPtr) guard let jsonData = jsonString.data(using: .utf8), let response = try? decoder.decode(FFISheetResponse.self, from: jsonData) else { return lines.enumerated().map { index, line in LineResult(id: index + 1, expression: line, result: "Error", isError: true) } } return response.results.enumerated().map { index, calcResult in mapToLineResult(calcResult, expression: lines[index], lineNumber: index + 1) } } // MARK: - Private private func evaluateSingleLine(_ line: String, lineNumber: Int) -> LineResult { let resultPtr = calcpad_eval_line(line) guard let resultPtr else { return LineResult(id: lineNumber, expression: line, result: "Error", isError: true) } defer { calcpad_free_result(resultPtr) } let jsonString = String(cString: resultPtr) guard let jsonData = jsonString.data(using: .utf8), let response = try? decoder.decode(FFIResponse.self, from: jsonData) else { return LineResult(id: lineNumber, expression: line, result: "Error", isError: true) } return mapToLineResult(response.result, expression: line, lineNumber: lineNumber) } private func mapToLineResult(_ calcResult: FFICalcResult, expression: String, lineNumber: Int) -> LineResult { switch calcResult.value { case .error(let message): // Blank/comment lines produce "empty expression" errors in the Rust engine. // Map these to nil result (matching the protocol's convention for non-evaluable lines). if isEmptyExpressionError(message) { return LineResult(id: lineNumber, expression: expression, result: nil, isError: false) } return LineResult(id: lineNumber, expression: expression, result: "Error: \(message)", isError: true) default: return LineResult(id: lineNumber, expression: expression, result: calcResult.metadata.display, isError: false) } } /// The Rust engine returns an error for blank lines, comments, and other /// non-evaluable input. We detect these to return nil (matching protocol convention). private func isEmptyExpressionError(_ message: String) -> Bool { let lower = message.lowercased() return lower.contains("empty") || lower.contains("no expression") || lower.contains("comment") } }