Author: Florent MERLET <[email protected]>
State: Draft
Type: Project
Vote: Pending
Created: 18-Aug-2026
Tcl-Version: 9.2
Tcl-Branch: tip-759_tcl90
Keywords: tcl, expr, operator, assignment, arithmetic, substitution
Version: $Revision: 2.2 $
Abstract
This project aims to facilitate arithmetic calculations in Tcl.
Rationale
Improving the readability of arithmetic calculations is a long term demand in Tcl. One of this first demand was TIP 282. Periodically, expansive discussions about it occur on the wiki. There were many suggestions to make things more practical : TIP 676, TIP 674, TIP 672.
A. Specifications
The features offered fall into three categories:
- Extensions to the expr language.
- An ability to assign values to variables (TIP 282 Assignment operator)
- An ability to chain calculations (TIP 282 Separating operator)
- Simplifying the definition of lists of numbers (Parentheses-comma list operator)
- Recognizing arithmetic substitution (Arithmetic TOKEN)
- Eventually (see part E below), recognizing some commands to control the flow (
for,while,foreach,lmap,...)
- More direct access from the command line (Arithmetic substitution)
- An ability to define arithmetic scripts : (Arithmetic compilation)
A.1 Extensions to the expr language
1.Assignement operator : credit is due to
Don Porter andWill Duquette . The implementation is derived from their initial proposal in TIP 282. A new binary assignement operator will be defined intoexprlanguage and will be=, it has right association rule.- A bareword will be allowed on its left, to be taken as the name of the variable to be set.
- If a math function is on the left of this
=operator then this is the result of this math function which will be taken as the name of the variable. Ex :expr {cos(0) = 1}will set the variable'1'to'1' - To set an array index, double-quotes (or braces) must be used like this :
expr {"arr(a)" = 0}, orexpr {{arr(a)} = 0}. Array indexes are not calculated by expr.
2.Separating operator : credit is also due to
Don Porter andWill Duquette . Current implementation is derived from their initial proposal in TIP 282 proposals.;will be defined as new binary operator, and allow to chain multiple instructions in oneexprcall. Additionnal properties will be included :- A trailing semi-colon mute the expression :
expr {i = 0;}returns nothing - It is not a mistake for a semi-colon to follow a colon operator :
expr {$a == 0 ? (a = 1) :;} - It is not a mistake for a semi-colon to follow a closing parenthesis :
expr {L=(1,2,3); $L} - It is not a mistake for a semi-colon to begin an expression :
expr {;(1+1)*2} - It is not a mistake for a semi-colon to follow an opening parenthesis :
expr {(;1+1)*2}(TBD) - It is not a mistake for a semi-colon to precede a closing parenthesis :
expr {(1;)} - It is legal that a semi-colon follow a semi-colon
expr {a=1;;b=2}
- A trailing semi-colon mute the expression :
3.parentheses-comma list operator :
- A comma separated list of element between parentheses will be taken as a list, ie :
set m [((1,0,0),(0,1,0),(0,0,1))]
will be made equivalent to :
set m [list [list 1 0 0] [list 0 1 0] [list 0 0 1]]]
- A comma separated list of element between parentheses will be taken as a list, ie :
4.Arithmetic TOKEN (see infra) : In the expr parser, any word begining by [( and finishing by )] will be subject to the arithmetic substitution :
expr {1+[(2*2)]}5.Include raw commands (see Part E) in
expr
A.2 Arithmetic substitution
At the Tcl parser level, as well as into the expr parser, any word begining by [( and finishing by )] will be subject to an arithmetic substitution : Tcl will produce a value according to the arithmetic language of the expr command.
Synopsis :
set A [( *expression* )]
This corresponds to this new rule of substitution :
- Arithmetic substitution : If the first caracter of a word is an opening bracket immediately followed by an opening parenthesis, then Tcl performs an Arithmetic substitution. The expression has to follow the rules of the expr mini-language. The word must finish at the next balanced closing-parenthesis wich is immediately followed by a closing-bracket.
Corner cases :
It is very important to note that not only the expression must begin and end with parentheses,
but that end is at the next balanced closing-parenthesis immediately followed by a closing bracket.
Ex :
set A [((3*4)+(2*5))]
* or && or ; are not enclosed by parentheses since the last closing parenthesis is balanced and followed by a closing bracket, it works.
set A [(3+4)*(2+5)];
set B [(a=1);(b=0);(c=0);(d=0)];
set C [($a || $b) && ($c || $d)];
set A [(3*4),(2*5)]; "(3*4),(2*5)]"
A.3 Arithmetic scripts
Any string subject to compilation that begins with an opening parenthesis ( and ends with a closing parenthesis ) will be bytecompiled according to the arithmetic language of the expr command.
This new feature shall be mentioned in the documentation for each of the Tcl commands that compiles some of their arguments (like after, apply, catch, dict filter, dict for, dict map, dict update, dict with, eval, for, foreach, if, interp eval, lmap, method, namespace code, namespace eval, namespace inscope, namespace unknown, proc, switch, try, while,... ), as well as for Tk commands (bind, canvas bind, console eval, consoleinterp eval, entry -vcmd, ttk::entry -validatecommand,...)
Synopsis :
eval {( *expression* )}
Corner cases :
Refer to Arithmetic Substitution above.
B. Examples of use
B.1 Extensions to the expr language examples
- assignement operator
# to set many vars once
expr {i = j = k = 0} - separating operator
# to swap two vars
expr {j = ( tmp = $j; $i); i = $tmp} - parentheses-comma list operator
# to return a list of element
expr {(sin($i), $j, cos($k)-1)}
B.2 Arithmetic substitution examples
Inline substitution (to get a value)
# Swaping two variables
lassign [($i,$j)] j i
# Drawing a rectangle on a canvas, knowing its center ($Cx,$Cy) coordinates,
# its height ($H) and its width ($L).
.c create rect [(
$Cx-$L/2.0, $Cy-$H/2.0,
$Cx+$L/2.0, $Cy+$H/2.0
)]
# A cross product proc
proc crossProduct {U V} {
lassign $U x y z
lassign $V u v w
[( $y*$w - $v*$z, $w*$x - $u*$z, $x*$v - $u*$y)]
}
% crossProduct {1 0 0} {0 1 0}# 0 0 1 Muted substitution (to return nothing, just create side effects)
# Setting many variables :
[( a = b = 0;
c = d = e = 1;
value = $c ; )]
B.3 Arithmetic script examples
apply
# Normal usage :
apply {{x y} {($x+$y)}} 1 2;# 3
# Or, without braces around the body :
apply {{x y} ($x+$y)} 1 2;# 3
# But not without space in the body :
apply {{x y} ($x + $y)} 1 2
# can't interpret "{x y} ($x + $y)"as a lambda expression (list of 4 elements)eval, namespace eval, interp eval
# Normal usage :
eval {(a = 1+1)};# 2
# Or, without brace, and since eval concats its arguments, adding space is not a problem :
eval ( a = 1 + 1 );# 2
# But, without brace, no ";" is allowed, since it is significant to the Tcl parser :
eval ( a = 1 + 1; b = 2 + 2 )
# -> invalid command name "("
# Conversely, "," isn't significant to the Tcl parser, then eval returns a list
eval ( a = 1 + 1, b = 2 + 2 );# 2 4
# Let's imagine a namespace with a lot of vars
namespace eval MyVars {(
A = 64;
B = 128;
C = $A | 1;
D = $B | 2;
)}
namespace eval MyVars {($A, $B, $C, $D)};# 64 128 65 130
# We may also use an interp
MyInterp eval {( A = 1; B = 2; C = $A+$B )} MyInterp eval {($A, $B, $C)}; # 1 2 3for, dict for
# for with arithmetic start or arithmetic step or arithmetic body. Ex :
for {set i 0} {$i<5} {incr i} {set v$i $i};# normal command
for {(i = 0)} {$i<5} {incr i} {set v$i $i};# arithmetic start
for (i=0) {$i<5} {(i = $i +1)} {set v$i $i};# arithmetic step
for (i=0) {$i<5} (i=\$i+1) {("v$i" = $i)};# arithmetic body
# if ever was existing a ++ operator...
for (i=0) {$i<5} (i++) {("v$i"=$i)} ;
# dict for / arithmetic body. Ex :
set D {x 100 y 200 z 300}
proc tcl::mathfunc::dset {dict key val} {
uplevel [list dict set $dict $key $val]
}
dict for {k v} $D {(
$k == "x" ? dset("D", $k, $v*6)
: $k == "y" ? dset("D", $k, $v*3)
: $k == "z" ? dset("D", $k, $v*2)
:;
)}
set D;# x 600 y 600 z 600 foreach, lmap, dict map
# foreach / arithmetic body. Ex : scalar product of 2 vectors
proc scalarProduct {U V} {
[(s = 0;)]
foreach u $U v $V {(s = $s + $u * $v )}
[($s)]
}
scalarProduct {1 2 3} {4 5 6};#32
# lmap / arithmetic body. Ex : tensor product of 2 vectors
proc tensorProduct {U V} {
lmap u $U {
lmap v $V {( $u * $v )}
}
}
tensorProduct {1 2 3} {4 5 6};# {4 5 6} {8 10 12} {12 15 18}
# dict map / arithmetic body. Ex : filter by key or value pattern
set D {hello Jack goodmorning Jay goodbye Harry}
proc dfilterK {D pattern} {
dict map {k v} $D {(
[string match $pattern $k] ? $v : [continue]
)}
}
proc dfilterV {D pattern} {
dict map {k v} $D {(
[string match $pattern $v] ? $v : [continue]
)}
}
dfilterK $D good*;#goodmorning Jay goodbye Harry
dfilterV $D Ja*;# hello Jack goodmorning Jay` if, switch
# if : arithmetic then / else clause
if 1 then (0) else (1);# 0
# switch : arithmetic case body
switch hello default ("goodbye");#goodbye proc (Examples adapted from Affine transforms on a canvas)
# proc : arithmetic body
# Script enclosed in braces : Tcl_Parser won't subst anything and we can add spacing
proc rotation angle {(
a=$angle/180.0*acos(-1);
c=cos($a), s=sin($a), -$s, $c, 0, 0
)}
# Script enclosed by double quotes : spaces can be added, but '$' sign must be backslashed
proc shear {sx sy} "(1, \$sx, \$sy, 1, 0, 0)"
# Script enclosed by nothing : spaces can't be added, and '$' sign must be backslashed
proc translate {dx dy} (0,0,0,\$dx,\$dy)
# Script enclosed by nothing can be usefull for pure list of number
proc reflect-x {} (1,0,0,-1,0,0)oo method
oo::object create O
oo::objdefine {variable o}
oo::objdefine O {method set {val} {variable o; set o $val}}
oo::objdefine O {method get {} {variable o; return $o}}
oo::objdefine O {method add {val} {( [variable o]; o=$o+$val )}}
% O set 1;#1
% O add 2;#3 bind
Example adapted from Centered coordinate System
pack [canvas .c -width 200 -height 200 -bd 0 -highlightthickness 0] -expand 1 -fill both
bind .c <Configure> {(
h=%h/2.0; w=%w/2.0;
[%W config -scrollregion [(-$w, -$h, $w, $h)] ]
)}source
Any file begining with(and finishing with)will be evaluated as an Arithmetic Script by the source command
For instance, given the file "ari.tcl" like :
(
! [info exist Vx] || ![info exist Vy] || ![info exist Vz] ? [
error "Vx, or Vy or Vz are not defined"
]:;
a00 = 0 ; a01 =-$Vz ; a02 = $Vy;
a10 = $Vz ; a11 = 0 ; a12 =-$Vx;
a20 =-$Vy ; a21 = $Vx ; a22 = 0 ;
($a00, $a01, $a02),
($a10, $a11, $a12),
($a20, $a21, $a22)
)
(tests) 1 % source ari.tcl
Vx, or Vy or Vz are not defined
(tests) 2 % [(Vx = 1; Vy = 2; Vz = 3;)]
(tests) 3 % source ari.tcl
{0 -3 2} {3 0 -1} {-2 1 0}
C Implementation details
Current state
The code volume is currently approximately 250 lines.
C.1 Extensions to the expr language implementation details
- TIP 282 Assignement operator
- in
tclCompExpr.c - in
preamble :- add
ASSIGNEMENTinLexemeCodes enum - add
PREC_ASSIGNinPrecedence enum, inchar prec[] table - add
/* ASSIGN */inchar instruction[] table
- add
- in func
parseExpr :- The code to detect if a bareword is followed by a
=sign and to add it to the literal list, so that it can be taken as a variable name during bytecode execution. - The code for right association rule.
- The declaration as not constant operator.
- The code to detect if a bareword is followed by a
- in func
parseLexem :- detection of
ASSIGNlexem
- detection of
- in func
CompileExprTree - add a
case ASSIGNto emitINST_STORE_STKbytecode instruction
- add a
- in
TIP 282 Separating operator
- in
tclCompExpr.c - in
preamble :- add
SEPARATORinLexemeCodes enum - add
PREC_SEPARATORinPrecedence enum, inchar prec[] table - add
/* SEPARATOR */inchar instruction[] table
- add
- in func
parseExpr :- Add code to allow some syntax
;;is not a syntax error (addNullelement inlitList):;is not a syntax error (addNullelement inlitList);)is not a syntax error (addNullelement inlitList);ENDis not a syntax error (addNullelement inlitList)
- Add code to allow some syntax
- in func
- add a
case SEPARATORto emitINST_POPbytecode instruction - add a
case SEPARATORto set the variableconvertto 0
- add a
- in
List Operator
- in
tclBasic.c - in
preamble :- add
"list", Tcl_ListObjCmdinBuiltinFuncTable[](mathfunc table)
- add
- in
- in
tclCompExpr.c - in
preamble :- add
NULL_FUNCinLexemeCodes enumas new unary operator - add
PREC_UNARYinPrecedence enum, inchar prec[] table - add
/* NULL_FUNC */inchar instruction[] table
- add
- in func
parseExpr :- create the list func name object
- (unary operator branche) create a
NULL_FUNCnode for eachOPEN_PARENnode, add "list" in thefunclist - When comma appear outside function argument list, change the
NULL_FUNClexeme of the node by aFUNCTIONlexeme.
- in
- in func
CompileExprTree - add a
case NULL_FUNCto skip the current funcList element
- add a
- in
C.2 Arithmetic substitution implementation details
- In
tclParse.c :- in
parseToken : Add a test for the existence of an open parenthese after the open bracket. Use theParse_Expr parser to get to the length of the arithmetic expression. Mark this area as aTCL_TOKEN_SUB_EXPR. - in
substToken : Add a case to check for aTCL_TOKEN_SUB_EXPRand then callTcl_ExprObj to substitute the token value.
- in
- In
tclCompExpr.c :- in
ParseExpr : Add a new variablesubstExpressionContext, count the parentheses, add code to to nest the shorthand into the shorthand, and to control the end of the expression - in
Tcl_ParseExpr : Manage the return of the length of the expression toparseToken
- in
- In
TclCompile.c :- in
TclCompileTokens : Add a case to handleTCL_TOKEN_SUB_EXPR
- in
- In
TclCompCmdsSZ.c :- in
TclSubstCompile : add aTCL_TOKEN_SUB_EXPRCase to compile the Token withTclCompileExpr (so that thesubstcommand can recognized the Arithmetic syntax)
- in
C.3 Arithmetic script examples implementation details
- In
tclParse.c :- In
Tcl_ParseCommand : At the begining, detect if the string begins with a(and finishes with a)[or)\nto be used with wish console], but not in anestedcontext. If so, fills the parse structure with aTCL_TOKEN_WORD, containing aTCL_TOKEN_SUB_EXPR
- In
- In
tclCompile.c :- In
TclSetBytcodesFromAny : Check if the string begins with a(and finishes with a), then compile it as an expression withTclCompileExpr . Avoid optimisation. - In
TclCompileScript : Check ifTcl_ParseCommand returns aTCL_TOKEN_WORDcontaining aTCL_TOKEN_SUB_EXPRtoken. If so, compile its component as an expression withTclCompileExpr .
- In
D. Justification of the choices made
D.1 Extensions to expr language
Why allow the left hand side of assignement operator to be a bareword ?
Why choose a list operator of the form ( , )?
The reason lies in the following analogy:
Command parameters come as a space-separated list
Arithmetic function parameters come as a comma-separated list.
Why not simply use a list function?
The reason is partly aesthetic and partly logical: an infix language based on binary operators is naturally more inclined toward symmetry than a prefix language.
{{1 2 3} {4 5 6} {7 8 9}};
[((1,2,3),(4,5,6),(7,8,9))];
[(list((1,2,3),(4,5,6),(7,8,9)))];
That said, the list func can also be used, since it has been included into the table of math function. But this will be longer, and, IMHO, less clear.
To make a comparaison, let's consider this example, taken from tcl lib math exact package. In the Object class TanWorker, we have this (long) command :
next [list \
[list \
[list [expr {2*$n + 1}] [expr {2*$n + 3}]] \
[list [expr {2*$n - 1}] [expr {2*$n + 1}]]] \
[list \
[list [expr {2*$n + 1}] [expr {2*$n - 1}]] \
[list [expr {2*$n + 3}] [expr {2*$n + 1}]]]] 0
With this TIP, using list function, we can write it :
next [(list(
list(list(2*$n + 1, 2*$n + 3), list(2*$n - 1, 2*$n + 1)),
list(list(2*$n + 1, 2*$n - 1), list(2*$n + 3,2*$n + 1))
))] 0
Whereas, using parentheses-comma operator, we can write :
next [(
((2*$n + 1, 2*$n + 3), (2*$n - 1, 2*$n + 1)),
((2*$n + 1, 2*$n - 1 ), (2*$n + 3, 2*$n + 1))
)] 0
To define clearly the data, in usual Tcl, we need to add all these 140 chars :
[list \ [list \ [list [expr {}] [expr {}]] \ [list [expr {}] [expr {}]]] \ [list \ [list [expr {}] [expr {}]] \ [list [expr {}] [expr {}]]]]
With this TIP, using the list function, we'll only need to add these 62 chars (NB : some syntactic unnecessary extra-spaces have been added in it solely for readability).
[(list( list(list(, ), list(, )), list(list(, ), list(, )) ))]
Whereas, with this TIP, but using the parentheses-comma operator instead, we'll only need to add these 32 chars (NB : some syntactic unnecessary extra-spaces have been added in it solely for readability).
[( ((, ), (, )), ((, ), (, )) )]
This makes it possible to measure the gain in conciseness offered by the parentheses-comma operator. In this specific case drawn from a Tcl library, the saving amounts to 108 characters compared to code using command syntax, and 30 characters compared to code using arithmetic syntax, but with the list function.
Why not having choosen to allow a comma to follow another comma ?
In expr language extension, a sequence of semi-colons is made to not be a syntax error :
Indeed, expr {;;;;} should not complain, since it is just a sequence of null operations.
I was thinking about allowing the same for comma, in the list operator context, so that expr {(,,)} would set the list {{} {} {}}
But I didn't do it. First, I was hesitating : should it set the list {0.0 0.0 0.0} instead ?
Moreover, I don't really like it when a program makes that kind of automatic assumption. It could mask genuine errors, which would then end up buried deep within the data—making them difficult to fix in practice.
D.2 Arithmetic substitution discussion and justifications
Arithmetic substitution appears in a context where the length of the arithmetic expression to parse is still unknown.
As you know, in Tcl, a command is parsed as a collection of words, each of which may contain many substitutions :
- Variable substitution,
TCL_TOKEN_VARIABLE - Command substitution,
TCL_TOKEN_COMMAND - Quote substitution,
TCL_TOKEN_QUOTED_WORD - backslash substitution,
TCL_TOKEN_BACKSLASH - brace substitution,
TCL_TOKEN_BRACED_WORD
Once the command is parsed, Tcl will substitute a value for each sub-TOKEN, all of it being appended to one Tcl_Obj per Word.
To create an arithmetic substitution, I choose to follow the same logic, that is, to define a new kind of token :
- Arithmetic substitution,
TCL_TOKEN_SUB_EXPR.
The problem in this substitution context is to know the char-length of the TOKEN. For this, in
Why not having choosed a prefix?
Ex : set a {=}1+1
As the name implies, a prefix precedes a word. For this reason, a prefix is detected within Tcl_Obj to serve as the command's argument.
It follows that, if arithmetic substitution were defined by a prefix, it would have been impossible to detect in the middle of a word. Consequently, the introductory syntax must be detected in the parseToken procedure.
set A value{=}1+1 ;
set A value[(1+1)];
Why wasn't the $(...) shortcut chosen?
Aside from the fact that this syntax makes it impossible to use the {} array—a usage that is well-established—the main reason is that, since the expr command is now multiline, an arithmetic substitution is more akin to calling a nested script than to referencing a variable. The $ sign would inevitably evoke the concept of a variable, thereby causing confusion.
Why wasn't the (...) shortcut choosen?
The use of parentheses—even though they are not significant to the Tcl_parser—remains very common, as they allow for the capture of an array index in variable assignment procedures (such as set, lappend, etc.). No one would want writing set a(1+2) 2 to assign a value to the scalar variable a2 instead of the key 1+1 in array a.
Why was an unicode pair of chars not choosen ?
A shorthand like ⟦...⟧ would be an excellent choice. The fact is, I lack the skills to implement it, hence the decision to stick to the ASCII character set. Furthermore, it is difficult for me to assess whether such a choice would be practical for embedded systems. However, if there is a demand for such a shorthand, a significant part of the implementation would be similar to the one proposed here.
Why not introduce a special command—like the one in TIP 676?
Tcl already possesses an excellent, battle-tested arithmetic expression compiler that integrates seamlessly with the language. I felt that introducing a new parser would be redundant and—given the subtly different syntax—a source of confusion.
Above all, however, I wanted to handle arithmetic expressions using purely syntactic means, as arithmetic is a fundamental element of any programming language.
That is why I proposed the concept of arithmetic substitution to reconcile arithmetic with Tcl's art of substitution.
Why was the double symbol [( ... )] chosen?
The fact is that parentheses are not significant to the Tcl_Parser, whereas they are significant to the expr parser due to the requirement that they be balanced.
Another point is that, while the opening bracket is recognized by the expr parser, the closing bracket is invalid there. This prevents any overflow within expr.
Consequently, in arithmetic substitution mode, one simply needs to count the parentheses until the count reaches zero. If the character following that final parenthesis is a closing bracket, one can infer that the arithmetic expression is well-formed. If, however, a closing bracket is encountered before that point, an "invalid character" error occurs, indicating a parenthesis imbalance.
Admittedly, using symbols that count as two characters makes the syntax more cumbersome. However, since the [()] syntax requires 4 characters, whereas nesting an [expr {}] command requires 9, there is still a gain in conciseness.
Another advantage relates to defining lists. With the expr command, 12 characters would be required—[expr {(,)}]—whereas the [(,)] shortcut requires only 5.
In short, this choice was driven by the simplicity and robustness of the implementation, while ensuring compliance with the specifications.
D.3 Arithmetic script :
The context of an arithmetic script differs from that of a substitution. It is contained within a Tcl_Obj resulting from prior parsing; consequently, the start and end of the string are known. There is no need to determine the string's length—as is required for substitutions—so there is no need to use
Since the code forms a single, contiguous block and cannot appear as a component of another token, syntax detection can be performed at the very beginning of
The
Why was it chosen that an arithmetic script should begin with ( and end with )?
Mostly by analogy with the choice of [(...)], previously made in the context of substitution. The form is similar, which will help the programmer remember it.
It helps also to shorten the usage of list operator in some situation.
% proc init {} ((0,0,0),(0,0,0),(0,0,0))
% init
{0 0 0} {0 0 0} {0 0 0}
This analogy in the definition thus allows for an analogy in the implementation.
E. Mixing command syntax and arithmetic syntax
As we can substitute commands into arithmetic,
and, conversely, arithmetic into commands,
that implies that it exists a lot of distinct ways to write the same program.
To illustrate this point, let's consider the following examples :
A command proc body :
edges = (($t1,$t2),($t2,$t3),($t3,$t1));
min_distance = 999999999
)];
foreach edge $edges {
lassign $edge p1 p2
lassign $p1 x1 y1
lassign $p2 x2 y2
A = $y2-$y1; B = $x1-$x2;
C = $x2*$y1 - $x1*$y2;
distance = abs($A * $px + $B * $py + $C) / hypot($A, $B);
$distance < $min_distance ? (min_distance = $distance) :;
)]
};
)]
};
% calculate_edge_distance_Cmd .5 .3 {0.0 0.0} {1.0 0.0} {0.0 1.0}
% timerate {calculate_edge_distance_Cmd .5 .3 {0.0 0.0} {1.0 0.0} {0.0 1.0}}
An arithmetic proc body
edges = (($t1,$t2),($t2,$t3),($t3,$t1));
min_distance = 999999999;
[
foreach edge $edges {
[
lassign $edge p1 p2
lassign $p1 x1 y1
lassign $p2 x2 y2
]
A = $y2-$y1; B = $x1-$x2;
C = $x2*$y1 - $x1*$y2;
distance = abs($A * $px + $B * $py + $C) / hypot($A, $B);
$distance < $min_distance ? (min_distance = $distance) :;
)
)
% calculate_edge_distance_Arith .5 .3 {0.0 0.0} {1.0 0.0} {0.0 1.0}
% timerate {calculate_edge_distance_Arith .5 .3 {0.0 0.0} {1.0 0.0} {0.0 1.0}}
Firstly, we can notice that the results are identical, and that the speed is similar.
Secondly, these two simple examples already show that a very long program could be written in a multitude of different ways to achieve exactly the same result.
That's why I think, if ever this TIP was accepted, a developer guideline should be produced to normalize the coding practices of mixing arithmetic syntax and command syntax.
Including commands to control the flow of computing into expr :
When, in arithmetic syntax context, we want control the flow, it is allways possible, thanks to the command substitution behaviour. But, if we need a body in arithmetic syntax, we'll have to use the script shorthand again. The consequence is then that we sometimes need 4 chars )}]; to close an arithmetic body if it is ending a command syntax context. Consider this example with a foreach and a while :
[
...
)
...
[
...
)
....
We can't say it is very beautifull. Can we make it lighter ?
Trying to get a lighter syntax with mathfunc
We could decide to add these control flow commands as mathfunc (or mathflows ?), in a manner, as they belong to the arithmetic context, that they would ask for an arithmetic body. For instance :
proc ::tcl::mathfunc::foreach {args} {
set body [lpop args]
uplevel [list ::foreach {*}$args ($body)]
}
proc ::tcl::mathfunc::while {test body} { uplevel [list ::while $test ($body)] }
Then, the previous example can be rewritten :
foreach("e", $L, "f", $M, {
...
});
...
while({$test}, {
...
});
But, the experiment shows that it is slow, mostly because of uplevel, but also because of the need to include the body into parentheses. Moreover, we have to quote every argument, and to separate them with commas.
Conclusion : There is no gain, neither in speed, neither in code concision. This is not the good way to go.
Trying to get a lighter syntax with modifying expr parser and tcl compiler
A prototype, which can be found in the branch tip-759b_tcl90, shows it is possible to get more concision without speed defect, by small modifications of the expr parser and the tcl compiler. Some command names can be recognized in ParseLexeme which will then return a new kind of leaf lexeme type : COMMAND. This leaf lexeme type can be used in ParseExpr, to parse the command, and its arguments till ... the next semi-colon ';', instead of till the next CLOSE_BRACKET ']'.
Finally, to compile this correctly, a new token type TCL_TOKEN_CMD_IN_EXPR has to be created (defined as TCL_TOKEN_SUB_EXPR | TCL_TOKEN_COMMAND), so that Tcl_CompileTokens can call Tcl_CompileScript with the good string boundaries. Here is an illustration of the result, taken from tcllib math package :
proc multPolyn_cmd {polyn1 polyn2} {
if { [llength $polyn1] == 1 && [string is double -strict $polyn1] } {
set polyn1 [polynomial $polyn1]
}
if { [llength $polyn2] == 1 && [string is double -strict $polyn2] } {
set polyn2 [polynomial $polyn2]
}
if { [lindex $polyn1 0] != "POLYNOMIAL" ||
[lindex $polyn2 0] != "POLYNOMIAL" } {
return -code error "Both arguments must be polynomials or a real number"
}
set coeffs1 [lindex $polyn1 1]
set coeffs2 [lindex $polyn2 1]
#
# Take care of the null polynomial
#
if { $coeffs1 == {} || $coeffs2 == {} } {
return [polynomial {}]
}
set zeros {}
foreach c $coeffs1 {
lappend zeros 0.0
}
set new_coeffs [lrange $zeros 1 end]
foreach c $coeffs2 {
lappend new_coeffs 0.0
}
set idx 0
foreach c $coeffs1 {
set term_coeffs {}
foreach c2 $coeffs2 {
lappend term_coeffs [expr {$c*$c2}]
}
set term_coeffs [concat [lrange $zeros 0 [expr {$idx-1}]] \
$term_coeffs \
[lrange $zeros [expr {$idx+1}] end]]
set sum_coeffs {}
foreach t $term_coeffs n $new_coeffs {
lappend sum_coeffs [expr {$t+$n}]
}
set new_coeffs $sum_coeffs
incr idx
}
return [list POLYNOMIAL $new_coeffs]
}
lmap, foreach and return as known commands in expr
proc multPolyn_ari {polyn1 polyn2} {(
[llength $polyn1] == 1 && [string is double -strict $polyn1] ? ( polyn1 = [polynomial $polyn1] ):;
[llength $polyn2] == 1 && [string is double -strict $polyn2] ? ( polyn2 = [polynomial $polyn2] ):;
[lindex $polyn1 0] != "POLYNOMIAL" || [lindex $polyn2 0] != "POLYNOMIAL" ?
[return -code error "Both arguments must be polynomials or a real number"] :;
coeffs1 = [lindex $polyn1 1];
coeffs2 = [lindex $polyn2 1];
#
# Take care of the null polynomial
#
$coeffs1 == {} || $coeffs2 == {} ? [ return [polynomial ""] ]:;
zeros = lmap c $coeffs1 {(0.0)};
new_coeffs = [lrepeat [( [llength $zeros]+[llength $coeffs2]-1 )] 0.0];
idx = 0;
foreach c $coeffs1 {(
term_coeffs = lmap c2 $coeffs2 {($c*$c2)};
term_coeffs = [lreplace $zeros $idx $idx {*}$term_coeffs];
sum_coeffs = lmap t $term_coeffs n $new_coeffs {($t+$n)};
new_coeffs=$sum_coeffs;
[incr idx];
)};
return [list POLYNOMIAL $new_coeffs];
)}
What the measurements shows, with P1 = {POLYNOMIAL {2 4 3}} and P2 = {POLYNOMIAL {3 2 4 5}}
| proc version | length | length diff | speed | speed diff |
|---|---|---|---|---|
| multPolyn_cmd | 1322 | +31.02 % | 10.19 µs | +17.13% |
| multPolyn_ari | 1009 | -23.67 % | 08.70 µs | -14.62% |
Conclusion : There is a gain, either in speed, either in code concision. This is the good way to go.
Incompatibilities :
- inline arithmetic substition : Any proc which is named '(' will be shadowed by the shorthand. However, it will still be possible to use it, either by protecting it by a backslash '\(', or simply, but less readable, by inserting a space between the open-bracket and the open-parenthese.
set A [\( protect with backslash to eval the '(' proc ] set B [ ( shift one space from the bracket to eval the '(' proc ] set C [eval [list ( make it as a list )]]
Comments / Questions
Schelte Bron :- Does the arithmetic substitution use the regular
exprfunction ?- Yes, the implementation use only functions that already exist in
Tcl:Tcl_ParseExpr,Tcl_CompileExpr,...
No new function has been added.
- Yes, the implementation use only functions that already exist in
- Is it possible to split in many TIP ?
- It is possible, but it implies then more work.
- Does the arithmetic substitution use the regular
Copyright
This document has been placed in the public domain.
