From fd64575799b0efbdc51fdc31de2728e7586a9c8d Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Thu, 2 Nov 2017 07:31:58 -0700 Subject: [PATCH 1/3] Handle statements in the condition of `while` --- NEWS | 1 + hy/compiler.py | 35 ++++++++++++++----- tests/native_tests/language.hy | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/NEWS b/NEWS index 4842f24..47f5c27 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,7 @@ Changes from 0.13.0 * Multiple expressions are now allowed in the else clause of a for loop * `else` clauses in `for` and `while` are recognized more reliably + * Statements in the condition of a `while` loop are repeated properly * Argument destructuring no longer interferes with function docstrings. [ Misc. Improvements ] diff --git a/hy/compiler.py b/hy/compiler.py index 8ca12ae..9870f6e 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -1861,12 +1861,33 @@ class HyASTCompiler(object): @checkargs(min=2) def compile_while_expression(self, expr): expr.pop(0) # "while" - ret = self.compile(expr.pop(0)) + cond = expr.pop(0) + cond_compiled = self.compile(cond) - orel = Result() - # (while cond body (else …)) + else_expr = None if ends_with_else(expr): else_expr = expr.pop() + + if cond_compiled.stmts: + # We need to ensure the statements for the condition are + # executed on every iteration. Rewrite the loop to use a + # single anonymous variable as the condition. + def e(*x): return HyExpression(x) + s = HySymbol + cond_var = s(self.get_anon_var()) + return self.compile(e( + s('do'), + e(s('setv'), cond_var, 1), + e(s('while'), cond_var, + # Cast the condition to a bool in case it's mutable and + # changes its truth value, but use (not (not ...)) instead of + # `bool` in case `bool` has been redefined. + e(s('setv'), cond_var, e(s('not'), e(s('not'), cond))), + e(s('if*'), cond_var, e(s('do'), *expr)), + *([else_expr] if else_expr is not None else []))).replace(expr)) # noqa + + orel = Result() + if else_expr is not None: for else_body in else_expr[1:]: orel += self.compile(else_body) orel += orel.expr_as_stmt() @@ -1874,11 +1895,9 @@ class HyASTCompiler(object): body = self._compile_branch(expr) body += body.expr_as_stmt() - ret += asty.While(expr, - test=ret.force_expr, - body=body.stmts, - orelse=orel.stmts) - + ret = cond_compiled + asty.While( + expr, test=cond_compiled.force_expr, + body=body.stmts, orelse=orel.stmts) ret.contains_yield = body.contains_yield return ret diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 42f27cb..ca93ecd 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -346,6 +346,68 @@ (assert (= a [2 "e"]))) +(defn test-while-multistatement-condition [] + + ; The condition should be executed every iteration, before the body. + ; `else` should be executed last. + (setv s "") + (setv x 2) + (while (do (+= s "a") x) + (+= s "b") + (-= x 1) + (else + (+= s "z"))) + (assert (= s "ababaz")) + + ; `else` should still be skipped after `break`. + (setv s "") + (setv x 2) + (while (do (+= s "a") x) + (+= s "b") + (-= x 1) + (when (= x 0) + (break)) + (else + (+= s "z"))) + (assert (= s "abab")) + + ; `continue` should jump to the condition. + (setv s "") + (setv x 2) + (setv continued? False) + (while (do (+= s "a") x) + (+= s "b") + (when (and (= x 1) (not continued?)) + (+= s "c") + (setv continued? True) + (continue)) + (-= x 1) + (else + (+= s "z"))) + (assert (= s "ababcabaz")) + + ; `break` in a condition applies to the `while`, not an outer loop. + (setv s "") + (for [x "123"] + (+= s x) + (setv y 0) + (while (do (when (and (= x "2") (= y 1)) (break)) (< y 3)) + (+= s "y") + (+= y 1))) + (assert (= s "1yyy2y3yyy")) + + ; The condition is still tested appropriately if its last variable + ; is set to a false value in the loop body. + (setv out []) + (setv x 0) + (setv a [1 1]) + (while (do (.append out 2) (setv x (and a (.pop a))) x) + (setv x 0) + (.append out x)) + (assert (= out [2 0 2 0 2])) + (assert (is x a))) + + (defn test-branching [] "NATIVE: test if branching" (if True From b25a69179f15c34c7e554e2f537784888cdd9e1e Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 27 Sep 2017 10:12:29 -0700 Subject: [PATCH 2/3] Update the documentation of `while` --- docs/language/api.rst | 52 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/docs/language/api.rst b/docs/language/api.rst index 63102c8..eb6550c 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -1800,13 +1800,61 @@ following shows the expansion of the macro. while ----- -``while`` is used to execute one or more blocks as long as a condition is met. -The following example will output "Hello world!" to the screen indefinitely: +``while`` compiles to a :py:keyword:`while` statement. It is used to execute a +set of forms as long as a condition is met. The first argument to ``while`` is +the condition, and any remaining forms constitute the body. The following +example will output "Hello world!" to the screen indefinitely: .. code-block:: clj (while True (print "Hello world!")) +The last form of a ``while`` loop can be an ``else`` clause, which is executed +after the loop terminates, unless it exited abnormally (e.g., with ``break``). So, + +.. code-block:: clj + + (setv x 2) + (while x + (print "In body") + (-= x 1) + (else + (print "In else"))) + +prints + +:: + + In body + In body + In else + +If you put a ``break`` or ``continue`` form in the condition of a ``while`` +loop, it will apply to the very same loop rather than an outer loop, even if +execution is yet to ever reach the loop body. (Hy compiles a ``while`` loop +with statements in its condition by rewriting it so that the condition is +actually in the body.) So, + +.. code-block:: clj + + (for [x [1]] + (print "In outer loop") + (while + (do + (print "In condition") + (break) + (print "This won't print.") + True) + (print "This won't print, either.")) + (print "At end of outer loop")) + +prints + +:: + + In outer loop + In condition + At end of outer loop with ---- From ed0b2735513dea5efd53a304ae1069dac0d646d7 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 27 Sep 2017 10:15:18 -0700 Subject: [PATCH 3/3] Fix a bug in docs/conf.py Without the trailing slash, `py = ('https://docs.python.org/3', None)` mysteriously creates links to the Python 2 documentation rather than Python 3. --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9877cc4..1513de0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,5 +52,5 @@ html_context = dict( hy_descriptive_version = hy_descriptive_version) intersphinx_mapping = dict( - py2 = ('https://docs.python.org/2', None), - py = ('https://docs.python.org/3', None)) + py2 = ('https://docs.python.org/2/', None), + py = ('https://docs.python.org/3/', None))