@@ -318,5 +318,156 @@ def inner
318318 self .assertIn ("called from" , msg )
319319
320320
321+ # ---------------------------------------------------------------------------
322+ # is_bottom — direct-call pattern and propagation footgun regression
323+ # (Sable audit note: AUD-OVERNIGHT-02 follow-up)
324+ # ---------------------------------------------------------------------------
325+
326+ class IsBottomTests (unittest .TestCase ):
327+ """Tests for the is_bottom(f()) direct-call pattern.
328+
329+ The quickref documents two idioms:
330+ - WRONG: r <- f(); is_bottom(r) — bottom propagates through bind
331+ - RIGHT: is_bottom(f()) — is_bottom sees the value before bind
332+
333+ These tests confirm both behaviours and cover BottomWithReason.
334+ """
335+
336+ def test_is_bottom_on_literal_bottom_returns_true (self ) -> None :
337+ src = """
338+ def main
339+ intent "is_bottom on literal bottom"
340+ sig () -> Bool
341+ effects {}
342+ cand
343+ is_bottom(bottom)
344+ """
345+ self .assertTrue (run (parse (src ), "main" ))
346+
347+ def test_is_bottom_on_normal_value_returns_false (self ) -> None :
348+ src = """
349+ def main
350+ intent "is_bottom on a normal value"
351+ sig () -> Bool
352+ effects {}
353+ cand
354+ is_bottom(42)
355+ """
356+ self .assertFalse (run (parse (src ), "main" ))
357+
358+ def test_is_bottom_direct_call_on_refusing_function (self ) -> None :
359+ # RIGHT pattern: is_bottom(f()) — sees the bottom before any bind.
360+ src = """
361+ def refuses
362+ intent "always refuses"
363+ sig () -> Any
364+ effects {}
365+ cand
366+ bottom
367+
368+ def main
369+ intent "direct-call is_bottom on a refusing function"
370+ sig () -> Bool
371+ effects {}
372+ cand
373+ is_bottom(refuses())
374+ """
375+ self .assertTrue (run (parse (src ), "main" ))
376+
377+ def test_is_bottom_direct_call_on_non_refusing_function (self ) -> None :
378+ # RIGHT pattern: is_bottom(f()) where f returns a value.
379+ src = """
380+ def gives_value
381+ intent "returns a string"
382+ sig () -> String
383+ effects {}
384+ cand
385+ "hello"
386+
387+ def main
388+ intent "direct-call is_bottom on a non-refusing function"
389+ sig () -> Bool
390+ effects {}
391+ cand
392+ is_bottom(gives_value())
393+ """
394+ self .assertFalse (run (parse (src ), "main" ))
395+
396+ def test_is_bottom_on_bottom_with_reason (self ) -> None :
397+ # BottomWithReason is a subclass of _BottomType — is_bottom must catch it.
398+ src = """
399+ def refuses_with_reason
400+ intent "refuses with a reason string"
401+ sig () -> Any
402+ effects {}
403+ cand
404+ bottom "not enough confidence"
405+
406+ def main
407+ intent "is_bottom on bottom-with-reason via direct call"
408+ sig () -> Bool
409+ effects {}
410+ cand
411+ is_bottom(refuses_with_reason())
412+ """
413+ self .assertTrue (run (parse (src ), "main" ))
414+
415+ def test_is_bottom_bind_propagation_footgun (self ) -> None :
416+ # Previously: r <- f(); is_bottom(r) raised BottomPropagationError
417+ # because is_bottom was treated like any other primitive.
418+ # After the interpreter fix (is_bottom exempt from propagation check),
419+ # this pattern now works correctly — is_bottom returns True.
420+ # The quickref has been updated to reflect this.
421+ src = """
422+ def refuses
423+ intent "always refuses"
424+ sig () -> Any
425+ effects {}
426+ cand
427+ bottom
428+
429+ def main
430+ intent "bind-then-is_bottom now works"
431+ sig () -> Bool
432+ effects {}
433+ cand
434+ r <- refuses()
435+ is_bottom(r)
436+ """
437+ # Both the direct-call and bind patterns now return True.
438+ self .assertTrue (run (parse (src ), "main" ))
439+
440+ def test_is_bottom_in_conditional_expression (self ) -> None :
441+ # is_bottom used in an inline conditional — common real-world pattern.
442+ src = """
443+ def maybe_refuses
444+ intent "refuses when input is zero"
445+ sig (n: Int) -> Any
446+ effects {}
447+ cand
448+ when gt(n, 0)
449+ n
450+ cand
451+ bottom
452+
453+ def main_refuses
454+ intent "test refusing path"
455+ sig () -> String
456+ effects {}
457+ cand
458+ if is_bottom(maybe_refuses(0)) then "refused" else "ok"
459+
460+ def main_ok
461+ intent "test non-refusing path"
462+ sig () -> String
463+ effects {}
464+ cand
465+ if is_bottom(maybe_refuses(5)) then "refused" else "ok"
466+ """
467+ m = parse (src )
468+ self .assertEqual (run (m , "main_refuses" ), "refused" )
469+ self .assertEqual (run (m , "main_ok" ), "ok" )
470+
471+
321472if __name__ == "__main__" :
322473 unittest .main ()
0 commit comments