Lines
100 %
Functions
Branches
//! BLOCK / RETURN-FROM lexical labelled exits.
use super::common::{
compile_and_validate, compile_expect_error, wrap_with_runtime_i32, wrap_with_runtime_ratio,
};
#[test]
fn block_returns_body_value_when_no_return_from() {
compile_and_validate("(block exit 7)");
}
fn block_with_multiple_body_forms_returns_last() {
compile_and_validate("(block exit 1 2 3)");
fn return_from_skips_remainder() {
compile_and_validate("(block exit (return-from exit 7) 99)");
fn return_from_inside_if() {
compile_and_validate(&wrap_with_runtime_ratio(
"(block exit (if (= X 0) (return-from exit 1) 2))",
));
fn nested_blocks_return_from_inner() {
compile_and_validate("(block outer (block inner (return-from inner 7) 99) 8)");
fn nested_blocks_return_from_outer_skips_outer_remainder() {
compile_and_validate("(block outer (block inner (return-from outer 7)) 99)");
fn return_from_unknown_block_errors() {
let err = compile_expect_error("(block exit (return-from missing 7))");
assert!(
err.contains("RETURN-FROM") && err.contains("MISSING"),
"expected lexical-scope error mentioning RETURN-FROM and 'MISSING', got: {err}"
);
fn return_from_outside_any_block_errors() {
let err = compile_expect_error("(return-from exit 7)");
err.contains("RETURN-FROM") && err.contains("no enclosing"),
"expected error about no enclosing BLOCK, got: {err}"
fn block_with_diverging_error_tail_types_from_string_return_from() {
// A `(block done (return-from done <str>) (error ...))` tail diverges
// (throws), so the block's value type must come from the RETURN-FROM
// exit (StringRef), not the `(error)` form's polymorphic I32. Before
// the reachable-exit redesign this was wrongly rejected as
// `string != i32`.
compile_and_validate(r#"(block done (return-from done "ok") (error 'boom "x"))"#);
fn block_with_diverging_error_tail_types_from_ratio_return_from() {
compile_and_validate("(block done (return-from done 11/10) (error 'boom \"x\"))");
fn block_with_only_error_body_compiles() {
compile_and_validate("(block done (error 'boom \"x\"))");
fn block_dead_tail_after_unconditional_return_from_is_typed_by_the_exit() {
// CL semantics: a form after an unconditional `(return-from)` is dead
// code. The block types from the reachable string exit; the ratio tail
// never executes, so this is valid (returns "ok"), not a type error.
compile_and_validate(r#"(block done (return-from done "ok") 11/10)"#);
fn block_types_from_live_exits_past_a_non_error_diverging_tail() {
// The tail `(tagbody loop (go loop))` loops forever, but it is dead
// anyway — the leading unconditional `(return-from)` already exits.
// The block must type from the reachable string exit, not the tail's
// `nil`/i32. Regression for the non-`(error)` diverging-tail gap.
compile_and_validate(r#"(block done (return-from done "ok") (tagbody loop (go loop)))"#);
fn block_dead_branch_return_from_does_not_taint_live_exit_type() {
// The `(if nil (return-from done "s") (return-from done 1/2))` test is
// statically false, so the string branch is dead; only the ratio exit
// is reachable. The block types as ratio — the dead string exit must
// not provoke a conflict.
compile_and_validate(
r#"(block done (if nil (return-from done "s") (return-from done 1/2)) (error 'boom "x"))"#,
fn block_quoted_return_from_is_data_not_an_exit() {
// A `(return-from foo …)` inside `(quote …)` is data, never compiled
// in value position, so emit-time discovery never records it as an
// exit. Otherwise the quoted ratio would clash with the `nil` tail.
compile_and_validate("(block foo (quote (return-from foo 1)) nil)");
compile_and_validate("(block foo '(return-from foo 1) nil)");
fn block_return_from_in_lambda_body_is_not_an_outer_exit() {
// A `(return-from foo …)` inside a `(lambda …)` / `(function …)` body
// compiles into a separate helper function, not the block's scratch
// buffer, so it records no exit for this lexical BLOCK (which is
// lexical-only). Its string "exit" must not clash with the numeric tail.
compile_and_validate(r#"(block foo (lambda () (return-from foo "x")) 0)"#);
compile_and_validate(r#"(block foo (function (lambda () (return-from foo "x"))) 0)"#);
fn block_return_from_in_cond_clause_test_position_is_collected() {
// A `(return-from out V)` in a COND clause's *test* position compiles
// (the test runs first), so emit-time discovery records its ratio exit
// and the block types from it rather than from the tail.
compile_and_validate("(block out (cond ((return-from out 1) 2)) 3)");
fn block_return_from_in_diverging_call_arg_types_the_block() {
// `(return-from b V)` as a call argument is compiled (args evaluate
// before the call), so it records an exit. The `(cons …)` never returns
// because its arg exits first; the ratio tail is dead. Emit-time
// discovery types the block from the reachable string exit — the
// syntactic pre-scan could not handle this.
compile_and_validate(r#"(block b (cons (return-from b "x") 2) 1)"#);
fn block_return_from_in_diverging_cons_cdr_arg_types_the_block() {
// Same as above but the divergence is in the CDR position: the car `2` is
// evaluated (for effect), then `(return-from b "x")` exits before the pair
// is built — so the non-list car/cdr is never a real cons and must NOT be
// rejected. The block types from the reachable string exit; the i32 tail is
// dead. Twin of the car-divergence case.
compile_and_validate(r#"(block b (cons 2 (return-from b "x")) 1)"#);
fn block_return_from_in_inline_lambda_call_arg_diverges() {
// The callee is an inline lambda (non-symbol head) — still an eager
// application, so the `(return-from b "x")` argument exits before the lambda
// body runs. Divergence must propagate through the non-symbol head so the
// i32 tail is recognized as dead and the block types from the string exit.
compile_and_validate(r#"(block b (cons ((lambda (x) 2) (return-from b "x")) nil) 1)"#);
// cdr-position twin.
compile_and_validate(r#"(block b (cons 2 ((lambda (x) nil) (return-from b "x"))) 1)"#);
fn block_return_from_in_discarded_macro_arg_is_not_an_exit() {
// A macro that discards its argument never compiles the
// `(return-from)` inside it, so no exit is recorded. The syntactic
// pre-scan wrongly rejected this (it could not know the arg was
// discarded without expanding the macro); emit-time discovery is exact.
r#"(defmacro ignore-arg (x) 0) (block b (ignore-arg (return-from b "x")) 1)"#,
fn block_dead_sequential_return_from_does_not_record_an_exit() {
// The second `(return-from)` is dead — the first unconditionally exits
// before it. BLOCK stops compiling at the first diverging form, so the
// dead string exit is never recorded and can't conflict with the live
// ratio exit. CL semantics: returns 1.
compile_and_validate(r#"(block b (return-from b 1) (return-from b "x"))"#);
fn block_many_flat_macro_siblings_do_not_exhaust_depth_guard() {
// The divergence walk expands macro heads to classify them. Many flat
// (nesting-depth-1) macro siblings must each start from fresh depth —
// the guard counts nesting, not total expansions — so 65 of them well
// exceed the depth-64 ceiling without wrongly erroring.
let calls = "(m) ".repeat(65);
compile_and_validate(&format!("(defmacro m () nil) (block x (begin {calls} 0))"));
fn block_macro_expanding_to_return_from_diverges() {
// A macro that expands to a bare `(return-from)` is itself diverging.
// BLOCK classifies divergence with one-step macro expansion (on a
// throwaway symbol table), so it stops before the dead second exit —
// which would otherwise record a conflicting type. CL result: 1.
r#"(defmacro ret1 () '(return-from b 1)) (block b (ret1) (return-from b "x"))"#,
fn block_conflicting_reachable_exits_error() {
// Two reachable `(return-from)` exits with different types (the IF test
// is a runtime value, so both branches are live) is a genuine type
// error, surfaced rather than silently picking one.
let err = compile_expect_error(&wrap_with_runtime_i32(
r#"(block done (if (= IDX 0) (return-from done "s") (return-from done 1/2)) (error 'boom "x"))"#,
err.contains("BLOCK") && err.contains("conflicting exit types"),
"expected a conflicting-exit-types error, got: {err}"
fn return_from_inside_wrapped_should_apply_resolves() {
// `should-apply`'s i32 body is wrapped in the Tier 3 boundary
// `try_table` (+3 block_depth). A `(return-from)` inside it must still
// resolve its relative `br` target correctly through the wrapper —
// this exercises the BLOCK machinery (not just a raw `br`) inside the
// wrapped valued entry point. The whole module must validate.
"(defun should-apply () (block done (if (= 1 1) (return-from done 1) 0)))",