Skip to content

Commit cf41030

Browse files
authored
Fix truthiness tests in if statements (#93)
1 parent 408d5bd commit cf41030

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

basilisp/compiler.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
_DO_PREFIX = 'lisp_do'
4040
_FN_PREFIX = 'lisp_fn'
4141
_IF_PREFIX = 'lisp_if'
42+
_IF_TEST_PREFIX = 'if_test'
4243
_MULTI_ARITY_ARG_NAME = 'multi_arity_args'
4344
_THROW_PREFIX = 'lisp_throw'
4445
_TRY_PREFIX = 'lisp_try'
@@ -710,7 +711,15 @@ def _fn_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
710711

711712
def _if_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
712713
"""Generate a function call to a utility function which acts as
713-
an if expression and works around Python's if statement."""
714+
an if expression and works around Python's if statement.
715+
716+
Every expression in Basilisp is true if it is not the literal values nil
717+
or false. This function compiles direct checks for the test value against
718+
the Python values None and False to accommodate this behavior.
719+
720+
Note that the if and else bodies are switched in compilation so that we
721+
can perform a short-circuit or comparison, rather than exhaustively checking
722+
for both false and nil each time."""
714723
assert form.first == _IF
715724
assert len(form) in range(3, 5)
716725

@@ -723,10 +732,22 @@ def _if_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
723732
else_nodes = [] # type: ignore
724733
lelse = ast.NameConstant(None) # type: ignore
725734

735+
test_name = genname(_IF_TEST_PREFIX)
736+
test_assign = ast.Assign(targets=[ast.Name(id=test_name, ctx=ast.Store())],
737+
value=_unwrap_node(test))
738+
726739
ifstmt = ast.If(
727-
test=_unwrap_node(test),
728-
body=[ast.Return(value=_unwrap_node(body))],
729-
orelse=[ast.Return(value=_unwrap_node(lelse))])
740+
test=ast.BoolOp(op=ast.Or(),
741+
values=[ast.Compare(left=ast.NameConstant(None),
742+
ops=[ast.Is()],
743+
comparators=[ast.Name(id=test_name, ctx=ast.Load())]),
744+
ast.Compare(left=ast.NameConstant(False),
745+
ops=[ast.Is()],
746+
comparators=[ast.Name(id=test_name, ctx=ast.Load())])
747+
]),
748+
values=[],
749+
body=[ast.Return(value=_unwrap_node(lelse))],
750+
orelse=[ast.Return(value=_unwrap_node(body))])
730751

731752
ifname = genname(_IF_PREFIX)
732753

@@ -739,7 +760,7 @@ def _if_ast(ctx: CompilerContext, form: llist.List) -> ASTStream:
739760
kwonlyargs=[],
740761
defaults=[],
741762
kw_defaults=[]),
742-
body=_unwrap_nodes(chain(test_nodes, body_nodes, else_nodes, [ifstmt])),
763+
body=_unwrap_nodes(chain(test_nodes, body_nodes, else_nodes, [test_assign, ifstmt])),
743764
decorator_list=[],
744765
returns=None))
745766
yield _node(ast.Call(

basilisp/core/__init__.lpy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@
144144
fname (if doc
145145
(with-meta name {:doc doc})
146146
name)
147-
body (rest body)
147+
body (if doc
148+
(rest body)
149+
body)
148150
args (if (vector? (first body))
149151
(first body)
150152
nil) ;; Should throw here!

tests/compiler_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,51 @@ def test_if(ns_var: Var):
281281
assert "YELLING" == lcompile(code)
282282

283283

284+
def test_truthiness(ns_var: Var):
285+
# Valid false values
286+
assert kw.keyword("b") == lcompile("(if false :a :b)")
287+
assert kw.keyword("b") == lcompile("(if nil :a :b)")
288+
289+
# Everything else is true
290+
assert kw.keyword("a") == lcompile("(if true :a :b)")
291+
292+
assert kw.keyword("a") == lcompile("(if 's :a :b)")
293+
assert kw.keyword("a") == lcompile("(if 'ns/s :a :b)")
294+
295+
assert kw.keyword("a") == lcompile("(if :kw :a :b)")
296+
assert kw.keyword("a") == lcompile("(if :ns/kw :a :b)")
297+
298+
assert kw.keyword("a") == lcompile("(if \"\" :a :b)")
299+
assert kw.keyword("a") == lcompile("(if \"not empty\" :a :b)")
300+
301+
assert kw.keyword("a") == lcompile("(if 0 :a :b)")
302+
assert kw.keyword("a") == lcompile("(if 1 :a :b)")
303+
assert kw.keyword("a") == lcompile("(if -1 :a :b)")
304+
assert kw.keyword("a") == lcompile("(if 1.0 :a :b)")
305+
assert kw.keyword("a") == lcompile("(if 0.0 :a :b)")
306+
assert kw.keyword("a") == lcompile("(if -1.0 :a :b)")
307+
308+
assert kw.keyword("a") == lcompile("(if () :a :b)")
309+
assert kw.keyword("a") == lcompile("(if '(0) :a :b)")
310+
assert kw.keyword("a") == lcompile("(if '(false) :a :b)")
311+
assert kw.keyword("a") == lcompile("(if '(true) :a :b)")
312+
313+
assert kw.keyword("a") == lcompile("(if [] :a :b)")
314+
assert kw.keyword("a") == lcompile("(if [0] :a :b)")
315+
assert kw.keyword("a") == lcompile("(if '(false) :a :b)")
316+
assert kw.keyword("a") == lcompile("(if '(true) :a :b)")
317+
318+
assert kw.keyword("a") == lcompile("(if {} :a :b)")
319+
assert kw.keyword("a") == lcompile("(if {0 0} :a :b)")
320+
assert kw.keyword("a") == lcompile("(if {false false} :a :b)")
321+
assert kw.keyword("a") == lcompile("(if {true true} :a :b)")
322+
323+
assert kw.keyword("a") == lcompile("(if #{} :a :b)")
324+
assert kw.keyword("a") == lcompile("(if #{0} :a :b)")
325+
assert kw.keyword("a") == lcompile("(if #{false} :a :b)")
326+
assert kw.keyword("a") == lcompile("(if #{true} :a :b)")
327+
328+
284329
def test_interop_call(ns_var: Var):
285330
assert lcompile('(. "ALL-UPPER" lower)') == "all-upper"
286331
assert lcompile('(.upper "lower-string")') == "LOWER-STRING"

0 commit comments

Comments
 (0)