Skip to content

jac format: multi-statement lambda bodies mash statements together with no whitespace #5523

@marsninja

Description

@marsninja

Bug

jac format on a lambda with multiple body statements concatenates the statements with no whitespace, producing broken output like { return undefined; }interval = setInterval(...);return lambda { clearInterval(interval);}; }.

This is a pre-existing bug in exit_lambda_expr — it predates the JSX/lambda formatter fixes in #5519. Reproducible on main as well as on the branch.

Repro

def main -> None {
    useEffect(
        lambda -> any {
            if not autoRefresh { return undefined; }
            interval = setInterval(lambda { loadMetrics(); }, 3000);
            return lambda { clearInterval(interval); };
        },
        [autoRefresh]
    );
}

Expected (something like)

def main -> None {
    useEffect(
        lambda -> any {
            if not autoRefresh {
                return undefined;
            }
            interval = setInterval(lambda { loadMetrics(); }, 3000);
            return lambda { clearInterval(interval); };
        },
        [autoRefresh]
    );
}

Actual (what jac format produces today)

def main -> None {
    useEffect(
        lambda -> any { if not autoRefresh { return undefined;  }interval = setInterval(
            lambda { loadMetrics();}, 3000
        );return lambda { clearInterval(interval);}; },
        [autoRefresh]
    );
}

Notice }interval and );return and ;}; all collapsed together with no separator.

Real-world hit

Surfaced while formatting jac-scale/jac_scale/admin/ui/pages/admin/monitoring/MetricsPage.cl.jac for the downstream reformat pass in #5519. Line 42 reads );return lambda { clearInterval(interval);}; }, after format, which is obviously wrong.

Root cause

exit_lambda_expr in jac/jaclang/compiler/passes/tool/impl/doc_ir_gen_pass.impl.jac iterates nd.kid and handles tokens, single-expression bodies, and the function signature explicitly, but when the body is a Sequence[CodeBlockStmt] (multi-statement lambda body), each statement kid falls through to:

} else {
    parts.append(i.gen.doc_ir);
}

No hard_line() separators are inserted between body statements and no indent wraps the body, so the statements run together. Every other statement-body node in the formatter (exit_if_stmt, exit_ability, function bodies, etc.) uses the pattern:

body_parts: list[doc.DocType] = [self.hard_line()];
prev_body_item: (uni.UniNode | None) = None;
for i in nd.kid {
    if (isinstance(nd.body, Sequence) and self.is_within(i, nd.body)) {
        if (i == nd.body[0]) {
            parts.append(self.indent(self.concat(body_parts), ast_node=nd));
            parts.append(self.hard_line());
        }
        self.add_body_stmt_with_spacing(body_parts, i, prev_body_item);
        prev_body_item = i;
    } elif ... {

exit_lambda_expr should grow the same branch gated on isinstance(nd.body, Sequence) and not isinstance(nd.body, uni.Expr), and drop the trailing hard-line on body_parts at the end so the closing } lands at the outer indent level.

What I tried

A draft fix that added the statement-body branch above to exit_lambda_expr produced correctly-separated output for the repro case and didn't regress the single-expression-body path (lambda x: int : x * 2) or the sole-statement path (lambda e: T { if cond { stmt; } } as in main.jac). Deliberately left out of #5519 to keep that PR scoped to JSX; the fix belongs in its own PR with dedicated regression tests covering:

  • Multi-statement lambda body with if/assignment/return
  • Single-statement lambda body (must still work flat inside a JSX attribute — the main.jac case)
  • Single-expression lambda body (lambda x: int : x * 2 must stay untouched)
  • Nested lambdas inside multi-statement lambda bodies

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions