Patchwork [v2] templater: provide arithmetic operations on integers

login
register
mail settings
Submitter Simon Farnsworth
Date Oct. 9, 2016, 12:53 p.m.
Message ID <2e2c959de0fe2c17bf6c.1476017605@devvm022.lla2.facebook.com>
Download mbox | patch
Permalink /patch/16990/
State Accepted
Headers show

Comments

Simon Farnsworth - Oct. 9, 2016, 12:53 p.m.
# HG changeset patch
# User Simon Farnsworth <simonfar@fb.com>
# Date 1476017464 25200
#      Sun Oct 09 05:51:04 2016 -0700
# Node ID 2e2c959de0fe2c17bf6c5f47c01035a36f13c593
# Parent  dbcef8918bbdd8a64d9f79a37bcfa284a26f3a39
templater: provide arithmetic operations on integers

The termwidth template keyword is of limited use without some way to ensure
that margins are respected.

Provide a full set of arithmetic operators (four basic operations plus the
mod function, defined to match Python's // for division), so that you can
create termwidth based layouts that match the user's terminal size
Yuya Nishihara - Oct. 9, 2016, 2:51 p.m.
On Sun, 9 Oct 2016 05:53:25 -0700, Simon Farnsworth wrote:
> # HG changeset patch
> # User Simon Farnsworth <simonfar@fb.com>
> # Date 1476017464 25200
> #      Sun Oct 09 05:51:04 2016 -0700
> # Node ID 2e2c959de0fe2c17bf6c5f47c01035a36f13c593
> # Parent  dbcef8918bbdd8a64d9f79a37bcfa284a26f3a39
> templater: provide arithmetic operations on integers

Nice. Queued slight modified version, thanks.

We'll need to catch ZeroDivisionError as a follow-up.

> --- a/mercurial/help/templates.txt
> +++ b/mercurial/help/templates.txt
> @@ -43,6 +43,12 @@
>  
>  .. functionsmarker
>  
> +We provide a limited set of infix arithmetic operations on integers:
> + + for addition
> + - for subtraction
> + * for multiplication
> + / for floor division (division rounded to integer nearest -infinity)
> +Division fulfils the law x = x / y + mod(x, y).

Reformatted as reST.

>  # methods to interpret function arguments or inner expressions (e.g. {_(x)})
>  exprmethods = {
>      "integer": lambda e, c: (runinteger, e[1]),
> +    "negate": lambda e, c: (runinteger, e[1]),

Looks like a copy-paste error. Removed this.

>      "string": lambda e, c: (runstring, e[1]),
>      "symbol": lambda e, c: (runsymbol, e[1]),
>      "template": buildtemplate,
> @@ -914,6 +950,11 @@
>      "|": buildfilter,
>      "%": buildmap,
>      "func": buildfunc,
> +    "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
> +    "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
> +    "negate": buildnegate,
> +    "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
> +    "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
>      }

> +But negate binds closer still:
> +
> +  $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
> +  (template
> +    (-
> +      ('integer', '1')
> +      (|
> +        ('integer', '3')
> +        ('symbol', 'stringify')))
> +    ('string', '\n'))
> +  hg: parse error: arithmetic only defined on integers
> +  [255]

For the record, this fails because '3' is taken as a keyword (for backward
compatibility), and evaluated to ''.
Simon Farnsworth - Oct. 9, 2016, 3:04 p.m.
On 09/10/2016 16:51, Yuya Nishihara wrote:
> On Sun, 9 Oct 2016 05:53:25 -0700, Simon Farnsworth wrote:
>> # HG changeset patch
>> # User Simon Farnsworth <simonfar@fb.com>
>> # Date 1476017464 25200
>> #      Sun Oct 09 05:51:04 2016 -0700
>> # Node ID 2e2c959de0fe2c17bf6c5f47c01035a36f13c593
>> # Parent  dbcef8918bbdd8a64d9f79a37bcfa284a26f3a39
>> templater: provide arithmetic operations on integers
>
> Nice. Queued slight modified version, thanks.
>
> We'll need to catch ZeroDivisionError as a follow-up.

Follow-up incoming to turn ZeroDivisionError into an abort (and to reuse 
runarithmetic() in mod()).

<snip>

>> +But negate binds closer still:
>> +
>> +  $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
>> +  (template
>> +    (-
>> +      ('integer', '1')
>> +      (|
>> +        ('integer', '3')
>> +        ('symbol', 'stringify')))
>> +    ('string', '\n'))
>> +  hg: parse error: arithmetic only defined on integers
>> +  [255]
>
> For the record, this fails because '3' is taken as a keyword (for backward
> compatibility), and evaluated to ''.
>
Indeed - the parse tree is the important bit here, not the failure, as 
it shows that we've parsed '1-3' as a subtraction with the right 
precedence, not as '1' then '-3'.

Patch

diff --git a/mercurial/help/templates.txt b/mercurial/help/templates.txt
--- a/mercurial/help/templates.txt
+++ b/mercurial/help/templates.txt
@@ -43,6 +43,12 @@ 
 
 .. functionsmarker
 
+We provide a limited set of infix arithmetic operations on integers:
+ + for addition
+ - for subtraction
+ * for multiplication
+ / for floor division (division rounded to integer nearest -infinity)
+Division fulfils the law x = x / y + mod(x, y).
 Also, for any expression that returns a list, there is a list operator::
 
     expr % "{template}"
diff --git a/mercurial/templater.py b/mercurial/templater.py
--- a/mercurial/templater.py
+++ b/mercurial/templater.py
@@ -33,6 +33,10 @@ 
     "|": (5, None, None, ("|", 5), None),
     "%": (6, None, None, ("%", 6), None),
     ")": (0, None, None, None, None),
+    "+": (3, None, None, ("+", 3), None),
+    "-": (3, None, ("negate", 10), ("-", 3), None),
+    "*": (4, None, None, ("*", 4), None),
+    "/": (4, None, None, ("/", 4), None),
     "integer": (0, "integer", None, None, None),
     "symbol": (0, "symbol", None, None, None),
     "string": (0, "string", None, None, None),
@@ -48,7 +52,7 @@ 
         c = program[pos]
         if c.isspace(): # skip inter-token whitespace
             pass
-        elif c in "(,)%|": # handle simple operators
+        elif c in "(,)%|+-*/": # handle simple operators
             yield (c, None, pos)
         elif c in '"\'': # handle quoted templates
             s = pos + 1
@@ -70,13 +74,8 @@ 
                 pos += 1
             else:
                 raise error.ParseError(_("unterminated string"), s)
-        elif c.isdigit() or c == '-':
+        elif c.isdigit():
             s = pos
-            if c == '-': # simply take negate operator as part of integer
-                pos += 1
-            if pos >= end or not program[pos].isdigit():
-                raise error.ParseError(_("integer literal without digits"), s)
-            pos += 1
             while pos < end:
                 d = program[pos]
                 if not d.isdigit():
@@ -420,6 +419,28 @@ 
             # If so, return the expanded value.
             yield i
 
+def buildnegate(exp, context):
+    arg = compileexp(exp[1], context, exprmethods)
+    return (runnegate, arg)
+
+def runnegate(context, mapping, data):
+    data = evalinteger(context, mapping, data,
+                        _('negation needs an integer argument'))
+    return -data
+
+def buildarithmetic(exp, context, func):
+    left = compileexp(exp[1], context, exprmethods)
+    right = compileexp(exp[2], context, exprmethods)
+    return (runarithmetic, (func, left, right))
+
+def runarithmetic(context, mapping, data):
+    func, left, right = data
+    left = evalinteger(context, mapping, left,
+                        _('arithmetic only defined on integers'))
+    right = evalinteger(context, mapping, right,
+                        _('arithmetic only defined on integers'))
+    return func(left, right)
+
 def buildfunc(exp, context):
     n = getsymbol(exp[1])
     args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
@@ -713,6 +734,20 @@ 
         tzoffset = util.makedate()[1]
     return (date[0], tzoffset)
 
+@templatefunc('mod(a, b)')
+def mod(context, mapping, args):
+    """Calculate a mod b such that a / b + a mod b == a"""
+    if not len(args) == 2:
+        # i18n: "mod" is a keyword
+        raise error.ParseError(_("mod expects two arguments"))
+
+    left = evalinteger(context, mapping, args[0],
+                        _('arithmetic only defined on integers'))
+    right = evalinteger(context, mapping, args[1],
+                        _('arithmetic only defined on integers'))
+
+    return left % right
+
 @templatefunc('revset(query[, formatargs...])')
 def revset(context, mapping, args):
     """Execute a revision set query. See
@@ -906,6 +941,7 @@ 
 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
 exprmethods = {
     "integer": lambda e, c: (runinteger, e[1]),
+    "negate": lambda e, c: (runinteger, e[1]),
     "string": lambda e, c: (runstring, e[1]),
     "symbol": lambda e, c: (runsymbol, e[1]),
     "template": buildtemplate,
@@ -914,6 +950,11 @@ 
     "|": buildfilter,
     "%": buildmap,
     "func": buildfunc,
+    "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
+    "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
+    "negate": buildnegate,
+    "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
+    "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
     }
 
 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
diff --git a/tests/test-command-template.t b/tests/test-command-template.t
--- a/tests/test-command-template.t
+++ b/tests/test-command-template.t
@@ -29,6 +29,111 @@ 
   $ hg merge -q foo
   $ hg commit -m 'merge' -d '1500001 0' -u 'person'
 
+Test arithmetic operators have the right precedence:
+
+  $ hg log -l 1 -T '{date(date, "%s") + 5 * 10} {date(date, "%s") - 2 * 3}\n'
+  1500051 1499995
+  $ hg log -l 1 -T '{date(date, "%s") * 5 + 10} {date(date, "%s") * 3 - 2}\n'
+  7500015 4500001
+
+Test division:
+
+  $ hg debugtemplate -r0 -v '{5 / 2} {mod(5, 2)}\n'
+  (template
+    (/
+      ('integer', '5')
+      ('integer', '2'))
+    ('string', ' ')
+    (func
+      ('symbol', 'mod')
+      (list
+        ('integer', '5')
+        ('integer', '2')))
+    ('string', '\n'))
+  2 1
+  $ hg debugtemplate -r0 -v '{5 / -2} {mod(5, -2)}\n'
+  (template
+    (/
+      ('integer', '5')
+      (negate
+        ('integer', '2')))
+    ('string', ' ')
+    (func
+      ('symbol', 'mod')
+      (list
+        ('integer', '5')
+        (negate
+          ('integer', '2'))))
+    ('string', '\n'))
+  -3 -1
+  $ hg debugtemplate -r0 -v '{-5 / 2} {mod(-5, 2)}\n'
+  (template
+    (/
+      (negate
+        ('integer', '5'))
+      ('integer', '2'))
+    ('string', ' ')
+    (func
+      ('symbol', 'mod')
+      (list
+        (negate
+          ('integer', '5'))
+        ('integer', '2')))
+    ('string', '\n'))
+  -3 1
+  $ hg debugtemplate -r0 -v '{-5 / -2} {mod(-5, -2)}\n'
+  (template
+    (/
+      (negate
+        ('integer', '5'))
+      (negate
+        ('integer', '2')))
+    ('string', ' ')
+    (func
+      ('symbol', 'mod')
+      (list
+        (negate
+          ('integer', '5'))
+        (negate
+          ('integer', '2'))))
+    ('string', '\n'))
+  2 -1
+
+Filters bind closer than arithmetic:
+
+  $ hg debugtemplate -r0 -v '{revset(".")|count - 1}\n'
+  (template
+    (-
+      (|
+        (func
+          ('symbol', 'revset')
+          ('string', '.'))
+        ('symbol', 'count'))
+      ('integer', '1'))
+    ('string', '\n'))
+  0
+
+But negate binds closer still:
+
+  $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
+  (template
+    (-
+      ('integer', '1')
+      (|
+        ('integer', '3')
+        ('symbol', 'stringify')))
+    ('string', '\n'))
+  hg: parse error: arithmetic only defined on integers
+  [255]
+  $ hg debugtemplate -r0 -v '{-3|stringify}\n'
+  (template
+    (|
+      (negate
+        ('integer', '3'))
+      ('symbol', 'stringify'))
+    ('string', '\n'))
+  -3
+
 Second branch starting at nullrev:
 
   $ hg update null
@@ -2890,14 +2995,15 @@ 
   $ hg debugtemplate -v '{(-4)}\n'
   (template
     (group
-      ('integer', '-4'))
+      (negate
+        ('integer', '4')))
     ('string', '\n'))
   -4
   $ hg debugtemplate '{(-)}\n'
-  hg: parse error at 2: integer literal without digits
+  hg: parse error at 3: not a prefix: )
   [255]
   $ hg debugtemplate '{(-a)}\n'
-  hg: parse error at 2: integer literal without digits
+  hg: parse error: negation needs an integer argument
   [255]
 
 top-level integer literal is interpreted as symbol (i.e. variable name):