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.
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) && cis compiled tob || a && c, which by JS operator precedence meansb || (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):Runtime demonstration (REPL):
Renders
true— should befalse. The compiled getter is() => b || a && c=true || (false && false)=true.Characterization
c && (b || a)c && b || a(c && b) || a(b || a) && cb || a && cb || (a && c)c && (a || b) && dc && a || b && d(c && a) || (b && d)c || (a && b)c || a && bThe 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!planescaped theisRematch &&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
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.