NSExpression error handling

Context: SwiftUI TextField with a String for simple math using NSExpression.

I first prepare the input string to an extent but a malformed input using valid characters still fails, as expected. Let's say preparedExpression is "5--"

let expr = NSExpression(format: preparedExpression)
 

gives

FAULT: NSInvalidArgumentException: Unable to parse the format string "5-- == 1"; (user info absent)

How can I use NSExpression such that either the preparedExpression is pre-tested before asking for actual execution or the error is handled in a polite way that I can use to alert the user to try again.

Is there a Swift alternative to NSExpression that I've missed?

Answered by DTS Engineer in 820388022

This error is an Objective-C language exception. For an explanation as to what that means, see What is an exception?

You can’t catch such exceptions in Swift. If you absolutely have to do that, your only option is to write an Objective-C wrapper that catches the exception and then returns it within an NSError. Oh, and that wrapper can’t use ARC, it has to be MRR (manual retain/release).

Having said that, the NSExpression parser wasn’t really designed to accept arbitrary user input. Using it that way can result in problems. For example:

  • If the expression uses a placeholder (%@) and you don’t supply one.

  • An expression can access arbitrary object properties via key paths.

If you want to turn user input into an expression, I recommend that you write or acquire your own parser.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

This error is an Objective-C language exception. For an explanation as to what that means, see What is an exception?

You can’t catch such exceptions in Swift. If you absolutely have to do that, your only option is to write an Objective-C wrapper that catches the exception and then returns it within an NSError. Oh, and that wrapper can’t use ARC, it has to be MRR (manual retain/release).

Having said that, the NSExpression parser wasn’t really designed to accept arbitrary user input. Using it that way can result in problems. For example:

  • If the expression uses a placeholder (%@) and you don’t supply one.

  • An expression can access arbitrary object properties via key paths.

If you want to turn user input into an expression, I recommend that you write or acquire your own parser.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for making that clear.

For the sake of helping anyone else with this issue, my solution is to use a pair of functions which cleans the typed expression as best it can but then calmly handles errors on any remaining malformed input

  private func handleSubmit() {
    print("handling submit")
    if let result = safelyEvaluateExpression() {
      inputExpression = result
      isError = false
    } else {
      isError = true
    }
  }
private func safelyEvaluateExpression() -> String? {
    let preparedExpression = inputExpression
      .replacingOccurrences(of: "×", with: "*")
      .replacingOccurrences(of: "÷", with: "/")
      .trimmingCharacters(in: .whitespacesAndNewlines)
    
    guard !preparedExpression.isEmpty else { return nil }
    
    let allowedChars = CharacterSet(charactersIn: "0123456789.+-*/() ")
    guard preparedExpression.unicodeScalars.allSatisfy({ allowedChars.contains($0) }) else {
      return nil
    }
    
    if let result = NSExpressionWrapper.evaluateExpression(preparedExpression) {
      return String(format: "%.2f", result.doubleValue)
    }
    return nil
  
  }

with:

//  NSExpressionWrapper.h
#import <Foundation/Foundation.h>

@interface NSExpressionWrapper : NSObject
+ (NSNumber *)evaluateExpression:(NSString *)string;
@end

and

//  NSExpressionWrapper.m
#import "NSExpressionWrapper.h"

@implementation NSExpressionWrapper
+ (NSNumber *)evaluateExpression:(NSString *)string {
  @try {
    NSExpression *expression = [NSExpression expressionWithFormat:string];
    return [expression expressionValueWithObject:nil context:nil];
  } @catch (NSException *exception) {
    // Log if needed
    NSLog(@"Caught exception: %@", exception.reason);
    return nil; // Return nil on error
  }
}
@end 

In the SwiftUI code I use isError to overlay the TextField to let the user know they have work to do.

Claude helped me with this code. I am not a programmer.

I'm disappointed that I need to use Obj-C code and not stick to Swift in order to allow simple inline math but perhaps with the advent of math in apps like Notes there will be a Swift solution in the future.

Written by pikes in 820792022
my solution is to use a pair of functions which cleans the typed expression as best it can but then calmly handles errors on any remaining malformed input

You’ve disable ARC on this Objective-C code, right?

Also, you kinda missed my point about placeholders. Consider this code:

@import Foundation;

int main(int argc, char **argv) {
    NSExpression * ex = [NSExpression expressionWithFormat:@"(1 + %@)"];
    return ex != nil ? 0 : 1;
}

On my machine this crashes with a memory access exception — a type of machine exception — which you can’t reasonably catch in any language. That’s because the %@ placeholder expects you to supply another argument with the value for that placeholder.

You can prevent the crash by calling the variant that takes an arguments array:

NSExpression * ex = [NSExpression expressionWithFormat:@"(1 + %@)" argumentArray:@[]];

This turns the crash into a language exception, which you’re already catching.

You also missed my point about key paths. Consider this code:

NSExpression * ex = [NSExpression expressionWithFormat:@"1.release" argumentArray:@[]];
id v = [ex expressionValueWithObject:nil context:nil];

Here I’m calling the Objective-C -release method via a key path. NSExpression expression explicitly forbids this and traps. That trap is actually a machine exception which you can’t reasonably catch in any language.

So, to sum up:

  • NSExpression is not designed to be used with an arbitrary user-supplied format string.

  • You may be able to patch some of the holes, but you’ll almost certainly miss some.

  • Rather than continue down this path, I recommend that you write or acquire your own expression parsing code [1].

And that brings me to:

Written by pikes in 820792022
perhaps with the advent of math in apps like Notes there will be a Swift solution in the future.

I recommend that you file an enhancement request for that. Honestly, I recommend that you file two ERs:

  • One against NSExpression, asking for Swift-safe version of +expressionWithFormat:.

  • One for a real Swift API.

Please post your bug numbers, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] If you’re looking for something cool, and your product is compatible with its licence, check out SoulverCore.

NSExpression error handling
 
 
Q