Repository: hylang/hy

Issue:
Conditionals waste assignments.
Conditionals containing statements are doing unnecessary work. I know, Python is not renowned for its performance, but let's not make it worse. This kind of thing can matter inside nested loops.

Currently a `cond` like this:

```
(cond [0 "zero"]
      [1 "one"]
      [True (assert 0)])
```

Turns into this:

```
if 0:
    _hy_anon_var_3 = 'zero'
else:
    if 1:
        _hy_anon_var_2 = 'one'
    else:
        if True:
            assert 0
            _hy_anon_var_1 = None
        else:
            _hy_anon_var_1 = None
        _hy_anon_var_2 = _hy_anon_var_1
    _hy_anon_var_3 = _hy_anon_var_2
```

But wouldn't it would work just as well like this?

```
if 0:
    _hy_anon_var_1 = 'zero'
elif 1:
    _hy_anon_var_1 = 'one'
elif True:
    assert 0
    _hy_anon_var_1 = None
```

Half the assignments for the same effect. And the `hy --spy` output is much more readable. I don't think CPython can automatically optimize this kind of thing. I know not all of them happen for any given branch, but the deeper ones will still do more assignments than they should.

Python has no switch/case. Sometimes you can get away with dictionary lookups instead, but if you need side effects or performance, you're supposed to use cascading `elif` where you would have used a switch. 

Currently there's no `elif` form in Hy. `cond` is the closest we've got. Now I could write nested `if`s instead (or a macro to do it for me) but look at what it turns into:

```
=> (if 0 "zero"
... (if 1 "one"
...       (assert 0)))
if 0:
    _hy_anon_var_2 = 'zero'
else:
    if 1:
        _hy_anon_var_1 = 'one'
    else:
        assert 0
        _hy_anon_var_1 = None
    _hy_anon_var_2 = _hy_anon_var_1
```

Not quite as bad as the `cond` since we have a final `else`, but still twice what is necessary:

```
if 0:
    _hy_anon_var_1 = 'zero'
elif 1:
    _hy_anon_var_1 = 'one'
else:
    assert 0
    _hy_anon_var_1 = None
```

Maybe it's too hard to optimize nested `if` forms like that? Is `cond` defined in terms of Hy's `if` form in the first place? I didn't implement `cond` but you could totally do it as a macro in terms of `if`.

Why not extend Hy's `if` to work like Arc Lisp's (as I proposed in #830 ), which can accept two or more arguments:

```
=> (if a b)
if a:
    _hy_anon_var_1 = b
else:
    _hy_anon_var_1 = None
=> (if a b
         c)
if a:
    _hy_anon_var_1 = b
else:
    _hy_anon_var_1 = c
=> (if a b
       c d)
if a:
    _hy_anon_var_1 = b
elif c:
    _hy_anon_var_1 = d
else:
    _hy_anon_var_1 = None
=> (if a b
       c d
         e)
if a:
    _hy_anon_var_1 = b
elif c:
    _hy_anon_var_1 = d
else:
    _hy_anon_var_1 = e
```

etc., for any number of inner `elif`s you need.
- Now you only have to generate AST for the new `if` form, instead of optimizing nested forms. 
- It make Hy's `if` act more like Python's cascading `elif`.
- This doesn't break existing Hy code, since we're not using more than 3 arguments now, and the 2 and 3 argument cases work exactly like before. 
- `cond` can be redefined in the same way, and get the same benefits.



Discussion:
I'm toying with just fixing up `if`s to avoid useless assignments.


Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -375,6 +375,7 @@ def __init__(self, module_name):
         self.anon_var_count = 0
         self.imports = defaultdict(set)
         self.module_name = module_name
+        self.temp_if = None
         if not module_name.startswith("hy.core"):
             # everything in core needs to be explicit.
             load_stdlib()
@@ -1001,11 +1002,41 @@ def _compile_catch_expression(self, expr, var):
     def compile_if(self, expression):
         expression.pop(0)
         cond = self.compile(expression.pop(0))
-
         body = self.compile(expression.pop(0))
+
         orel = Result()
+        nested = root = False
         if expression:
-            orel = self.compile(expression.pop(0))
+            orel_expr = expression.pop(0)
+            if isinstance(orel_expr, HyExpression) and isinstance(orel_expr[0],
+               HySymbol) and orel_expr[0] == 'if':
+                # Nested ifs: don't waste temporaries
+                root = self.temp_if is None
+                nested = True
+                self.temp_if = self.temp_if or self.get_anon_var()
+            orel = self.compile(orel_expr)
+
+        if not cond.stmts and isinstance(cond.force_expr, ast.Name):
+            name = cond.force_expr.id
+            branch = None
+            if name == 'True':
+                branch = body
+            elif name in ('False', 'None'):
+                branch = orel
+            if branch is not None:
+                if self.temp_if and branch.stmts:
+                    name = ast.Name(id=ast_str(self.temp_if),
+                                    arg=ast_str(self.temp_if),
+                                    ctx=ast.Store(),
+                                    lineno=expression.start_line,
+                                    col_offset=expression.start_column)
+
+                    branch += ast.Assign(targets=[name],
+                                         value=body.force_expr,
+                                         lineno=expression.start_line,
+                                         col_offset=expression.start_column)
+
+                return branch
 
         # We want to hoist the statements from the condition
         ret = cond
@@ -1013,7 +1044,7 @@ def compile_if(self, expression):
         if body.stmts or orel.stmts:
             # We have statements in our bodies
             # Get a temporary variable for the result storage
-            var = self.get_anon_var()
+            var = self.temp_if or self.get_anon_var()
             name = ast.Name(id=ast_str(var), arg=ast_str(var),
                             ctx=ast.Store(),
                             lineno=expression.start_line,
@@ -1026,10 +1057,12 @@ def compile_if(self, expression):
                                col_offset=expression.start_column)
 
             # and of the else clause
-            orel += ast.Assign(targets=[name],
-                               value=orel.force_expr,
-                               lineno=expression.start_line,
-                               col_offset=expression.start_column)
+            if not nested or not orel.stmts or (not root and
+               var != self.temp_if):
+                orel += ast.Assign(targets=[name],
+                                   value=orel.force_expr,
+                                   lineno=expression.start_line,
+                                   col_offset=expression.start_column)
 
             # Then build the if
             ret += ast.If(test=ret.force_expr,
@@ -1052,6 +1085,10 @@ def compile_if(self, expression):
                              orelse=orel.force_expr,
                              lineno=expression.start_line,
                              col_offset=expression.start_column)
+
+        if root:
+            self.temp_if = None
+
         return ret
 
     @builds("break")


Test patch: