@dynamicCallable with string interpolation arguments cause compilation error

I tried to test drive @dynamicCallable by writing a Logger class but ran into a surprising problem - the playground reports:
error: cannot use mutating member on immutable value: '$interpolation' is immutable
and the compiler reports:
<unknown>:0: error: 'inout DefaultStringInterpolation' is not convertible to 'DefaultStringInterpolation'

here's the code:

@dynamicCallable class Logger { func dynamicallyCall(withArguments args: [Any]) -> Void { var output = "" args.forEach { print($0, separator: " ", terminator: "", to: &output) } print(output) } } var log = Logger() let a = 1 log(a) // prints 1 log("a") // prints a log("\(a)") // error: 'inout DefaultStringInterpolation' is not convertible to 'DefaultStringInterpolation' log.dynamicallyCall(withArguments: ["\(a)"]) // prints 1 

I managed to get string interpolation working with the logger by creating a class that implements ExpressibleByStringInterpolation.StringInterpolation as a class by modifying so:

public final class LogElement: ExpressibleByStringInterpolation, CustomStringConvertible { public let description: String public init(stringLiteral value: String) { self.description = value } public init(stringInterpolation: StringInterpolation) { description = stringInterpolation.output } final public class StringInterpolation: StringInterpolationProtocol { var output = "" public init(literalCapacity: Int, interpolationCount: Int) { output.reserveCapacity(literalCapacity * 2) } public func appendLiteral(_ literal: String) { output.append(literal) } public func appendInterpolation(_ string: Any) { output.append(String(describing: string)) } } } @dynamicCallable class Logger { func dynamicallyCall(withArguments args: [LogElement]) -> Void { var output = "" args.forEach { print($0, separator: " ", terminator: "", to: &output) } print(output) } } 

However, this loses the crucial args: [Any] parameter that allows it to log all values.

Is this expected behaviour? Are there any other workarounds?

Cheers,
Daniel

This is a known issue, SR-10753. Sorry about that!

@jrose thanks for the speedy response. Googling didn't find that - too many hits with @dynamicCallable and ExpressibleByStringInterpolation in swift 5!

It looks like my patch from a while ago unintentionally fixed this? [CSApply] Restructure the implicit AST when applying @dynamicCallable by Azoy · Pull Request #23845 · apple/swift · GitHub

1 Like

Well, great, let's retest it and merge it! :-)

I misused ExpressibleByArray in order to coerce [Any] to my implementation. I also faked a #function defaulted parameter to report the call site function. See this thread for a discussion of #file #function literal defaults.

Here's the crufty code:

public final class LogElement: CustomStringConvertible, ExpressibleByStringInterpolation, ExpressibleByArrayLiteral { let elements: [Any] public let description: String public init(arrayLiteral elements: Any...) { self.elements = elements switch elements.count { case 0: self.description = "()" case 1: if let element = elements[0] as? String { self.description = "\"\(element)\"" } else { self.description = "\(elements[0])" } case _: self.description = "Warning: please only use a single element in a LogElement array literal!\n\(elements)" } } public init(stringLiteral value: String) { self.description = "\"\(value)\"" self.elements = [value] } public init(stringInterpolation: StringInterpolation) { description = stringInterpolation.output self.elements = [stringInterpolation.output] } final public class StringInterpolation: StringInterpolationProtocol { var output = "" public init(literalCapacity: Int, interpolationCount: Int) { output.reserveCapacity(literalCapacity * 2) } public func appendLiteral(_ literal: String) { output.append(literal) } public func appendInterpolation(_ value: Any, label: String = "") { if label.isEmpty == false { output.append("\(label): ") } if let string = value as? String { output.append("\"\(string)\"") } else { output.append("\(value)") } } } } @dynamicCallable public struct Logger { public init() { } private (set) public var doPrintFunction: Bool = false public mutating func enableFunctionHeader() { doPrintFunction = true } public mutating func disableFunctionHeader() { doPrintFunction = false } public func dynamicallyCall(withArguments args: [LogElement]) { printFunctionName() let output = args.map { "\($0)" }.joined(separator: ", ") print("ℹ️", output) } public func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, LogElement>) { printFunctionName() var args = args.toArray() if let fileIndex = args.firstIndex(where: { $0.key == "file" }), let filePath = args[fileIndex].value.elements.first as? String { let url = URL(fileURLWithPath: filePath) args[fileIndex] = ("file", "\(url.lastPathComponent)") } let output = args.map { "\($0.key): \($0.value)" }.joined(separator: ", ") print("ℹ️", output) } private let guessCallSiteStackDistance = 3 @inline(__always) // Doesn't seem to work in Debug. private func printFunctionName() { guard doPrintFunction else { return } if let callSiteSymbol = Thread.callStackSymbols.prefix(guessCallSiteStackDistance).last, let mangledSignature = callSiteSymbol.split(separator: " ").prefix(4).last.map({ String($0) }) { if let functionName = try? parseMangledSwiftSymbol(mangledSignature, isType: false).print(using: .simplified), let preceding = functionName.lastIndex(of: "."), let successor = functionName.firstIndex(of: "(") { let start = functionName.index(after: preceding) let end = functionName.index(before: successor) print("🔹", functionName[start...end], "(): ", separator: "", terminator: " ") } } } } class DynamicLoggerTests: XCTestCase { func testDynamicLogger() { var log = Logger() log("simply literal") log("interp\(0)lat\(1)\(0)n") oneLevelDeeper(passing: log) log(file: #file, isEmpty: [], isArray: [[1,2,3]]) log.enableFunctionHeader() log(isNested: "\("nesting") \("works")", butOnlyOneLevelDeep: "🤯") // "\("This \("fails")")") } func oneLevelDeeper(passing log: Logger) { var log = log log("\(0...1, label: "closed")", "\(2..<3, label: "halfOpen")") log(partialUpTo: [...4], partialFrom: [5...]) log.enableFunctionHeader() log(shouldPrintCallingFunction: "Shame we can't have #file:#line propogated") } } // prints to console: // ℹ️ "simply literal" // ℹ️ interp0lat10n // ℹ️ closed: 0...1, halfOpen: 2..<3 // ℹ️ partialUpTo: PartialRangeThrough<Int>(upperBound: 4), partialFrom: PartialRangeFrom<Int>(lowerBound: 5) // 🔹oneLevelDeeper(): ℹ️ shouldPrintCallingFunction: "Shame we can't have #file:#line propogated" // ℹ️ file: "ChangesetRegressionData.swift", isEmpty: (), isArray: [1, 2, 3] // 🔹testDynamicLogger(): ℹ️ isNested: "nesting" "works", butOnlyOneLevelDeep: "🤯"