TIP 526: Make [expr] Only Accept One Argument

Login
Author:         Donal K. Fellows <donal.k.fellows@manchester.ac.uk>
State:          Draft
Type:           Project
Vote:           Pending
Created:        08-Nov-2018
Tcl-Version:    9.0
Post-History:

Abstract

This TIP proposes that expr should only accept a single argument and stop accepting and concatenating arguments.

Rationale and Discussion

A very common fault of expr usage is to fail to brace expressions. This was semi-good practice in Tcl 7.6 and before as Tcl's substitution phase was faster than the expression evaluation engine's, but it was always a little tricky even then as it depended on you being sure that the substituted values were numeric for safety. When that critical side condition didn't hold, it resulted in unsafe code:

proc addFive {x} {
    expr $x + 5
}
puts [addFive 17];                 # ==> 22
puts [addFive {[exec format C:]}]; # OOPS!

From Tcl 8.0 onwards, this was no longer a good idea. The bytecode compiler is able to generate compiled code ahead of time for braced expressions, but is not able to do so (usefullly) if any of the arguments to expr are unbraced. Of course, we want to be able to use an unbraced argument occasionally, such as when we take an expression as an argument from elsewhere, but those are very much the unusual case: almost always, a call to expr with any argument unbraced is both a performance bug and a security bug.

Because this is so strongly the case, there are uses of Tcl that are almost always a problem; for example:

expr $x >= "inf";   # where $x is expected to be a string

Proposal

To reduce the incidence of this, I propose that in Tcl 9.0 the expr command should only take a single argument. It most definitely isn't a total fix for this — a total fix isn't really possible without making the expr command special in the Tcl base language, and that goes against the general principle that nothing is special there — but it will at least reduce the incidence a bit by encouraging people to either brace their expressions or be very clear that they are not doing so.

This will leave virtually all correct code working, and yet force code that has a problem to acknowledge the fact.

Supported cases:

  1. Braced expressions:

    expr {$x * 5}
    
  2. Expressions in variables:

    set theExpression {$x * 5}
    expr $theExpression
    
  3. Expressions from commands:

    expr [dict get $myExpressions thisCase]
    

Cases that work because we can't stop them without breaking the language:

  1. Expressions in double quotes:

    expr "$x * 5"
    
  2. Expressions with unquoted compound substitutions:

    expr $x*5+[llength $y]
    

Cases that are outright rejected:

  1. Expressions with multiple arguments:

    expr $x * 5
    
  2. Expressions with multiple literal arguments (uncommon):

    expr {$x} *5
    

Workarounds for Broken Old Code

The cases where someone genuinely needs the old behaviour can be handled by them using concat (or string cat, depending on exact requirements) explicitly to make the expression:

set x {2 + 3}
expr [concat $x * 5]

That still has the faults, but now at least looks like it is doing something tricky. Which it is, given that the actual behaviour of the equivalent code in Tcl 8 is so messy. Another alternative (and a better one in this particular case) is:

set x {2 + 3}
expr {[expr $x] * 5}

That also looks weird, but is better in that it only has the potential for trouble in one place and allows for better compilation of the outer parts of the expression. (Yes, it produces a different result from the other code. This is likely to be a good thing in almost all cases.)