Skip to content

Compiler drops precedence-significant parentheses in logical expressions ((a || b) && ca || b && c) #18369

Description

@gati3478

Describe the bug

The compiler drops precedence-significant parentheses from logical (&&/||) expressions, generating valid-but-semantically-wrong JavaScript. Any parenthesized || group used as an operand of && is corrupted.

For example, (b || a) && c is compiled to b || a && c, which by JS operator precedence means b || (a && c) — a different boolean. This silently produces incorrect runtime values (not a syntax error), so it fails quietly.

This happens in every expression context: $derived(...), $: reactive statements, and inline template attributes (disabled={...}).

It is distinct from the closed Svelte 3 issue #5558 (that one was about ?? mixing causing a syntax error; this one produces valid but wrong code).

Reproduction

Compiling with svelte/compiler (5.56.1):

import { compile } from 'svelte/compiler';
const src = `<script>let a=$state(false), b=$state(true), c=$state(false); const x=$derived((b||a)&&c);</script>{x}`;
console.log(compile(src, { generate: 'client', runes: true }).js.code);
// ->  const x = $.derived(() => b || a && c);     // parens dropped: now (b || (a && c))

Runtime demonstration (REPL):

<script>
  let a = $state(false);
  let b = $state(true);
  let c = $state(false);
  // intent: (b || a) && c  =>  (true || false) && false  =>  false
  const result = $derived((b || a) && c);
</script>
<p>{result}</p>

Renders true — should be false. The compiled getter is () => b || a && c = true || (false && false) = true.

Characterization

Source expression Compiled output Correct?
c && (b || a) c && b || a ❌ becomes (c && b) || a
(b || a) && c b || a && c ❌ becomes b || (a && c)
c && (a || b) && d c && a || b && d ❌ becomes (c && a) || (b && d)
c || (a && b) c || a && b ✅ (parens were redundant)

The parens are only safely removable when they were redundant under default precedence (e.g. || over a && group). When they override precedence — a || group as an operand of && — they must be preserved.

Real-world impact

This shipped a real bug in our app: a button's disabled={isImporting || netDisabled || (isRematch && (isPlanning || !plan))} compiled such that !plan escaped the isRematch && guard, leaving the button permanently disabled in the non-rematch path. It took an e2e failure + dumping live values to track down, because each individual operand looked correct.

Severity

severity: blocking — silent miscompilation of a fundamental language construct; no error surfaces.

System Info

Svelte: 5.56.1 (latest on npm)
Node: v24.16.0
Compiler options: { generate: 'client', runes: true }

The Svelte 5 expression printer (esrap-based codegen) appears to omit parentheses based on operator precedence but mishandles the case where a lower-precedence group is an operand of a higher-precedence operator.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions