WEP: Operator Precedence and Associativity
Context
Wado needs to define operator precedence and associativity rules. The language aims to minimize learning costs while avoiding design mistakes present in older languages. Several key decisions were needed:
- Overall precedence table: Which existing language to use as a baseline?
- Bitwise vs comparison operators: Which should have higher precedence?
- Bitwise NOT operator: Use
!(Rust) or~(C/Java/Python)? - Increment/decrement operators: Include
++/--or not? - Power operator: Include
**or not? - Comparison chaining: Allow
a < b < cor make it an error?
C's Design Mistake
C has a well-known precedence design flaw: bitwise operators (&, |, ^) have lower precedence than comparison operators (==, <, etc.). This causes bugs:
// C/Java/JavaScript - Bug!
if (flags & MASK == EXPECTED) // Parsed as: flags & (MASK == EXPECTED)
// Intended: (flags & MASK) == EXPECTED
Historical reason: Early C had no &&/||. When they were added, &/| precedence was left unchanged for backward compatibility, creating a permanent design flaw.
Impact: Requires excessive parentheses and is counterintuitive since & and | conceptually behave like arithmetic operators:
a + b == 7correctly parses as(a + b) == 7✅a & b == 7incorrectly parses asa & (b == 7)❌
Language Comparison
| Language | Bitwise vs Comparison | Fixed C's Mistake? |
|---|---|---|
| C/Java/JS | Comparison > Bitwise | ❌ No |
| Rust | Bitwise > Comparison | ✅ Yes |
| Go | Bitwise > Comparison | ✅ Yes |
| Python | Comparison > Bitwise | ❌ No |
Rust and Go fixed this by giving bitwise operators higher precedence than comparison operators.
Increment/Decrement Operators
++/-- cause serious issues:
int x = 1;
int y = x + ++x; // Undefined behavior!
// GCC: y = 4
// Clang: y = 3
Problems:
- Undefined behavior when used multiple times in same expression
- Side effect confusion
- Prefix vs postfix complexity
Language responses:
- Rust: Removed entirely ✅
- Python: Never had them ✅
- Go: Postfix only, statements only (not expressions) ⚠️
- C/Java/JS: Keep them (legacy) ❌
Power Operator
Python's ** has counterintuitive precedence:
-1**2 # = -1 (not 1!)
# Parsed as: -(1**2)
Language approaches:
- Python/JS: Have
**operator (with precedence quirks) ⚠️ - C/Java/Rust/Go: Use function:
pow(),Math.pow(),.pow()✅
Comparison Chaining
Different languages handle chained comparisons differently:
// JavaScript - Bug prone
1 < 2 < 3; // true (seems right)
3 > 2 > 1; // false (wait, what?)
// Evaluates as: (3 > 2) > 1 → true > 1 → 1 > 1 → false
Language approaches:
- C/Java/Go/JS: Left-associative (allows confusing bugs) ❌
- Rust: Non-associative (compile error) ✅
- Python: Special chaining syntax (
a < b < c=a < b and b < c) ✅
Decision
1. Use Rust's Precedence as Baseline
Wado adopts Rust's operator precedence table with minor modifications.
Rationale:
- Fixes C's bitwise precedence mistake
- No
++/--(avoids undefined behavior) - Well-designed and battle-tested
- Minimizes learning costs (developers familiar with Rust can transfer knowledge)
2. Bitwise Operators Have Higher Precedence Than Comparison
Following Rust and Go, bitwise operators (&, |, ^) have higher precedence than comparison operators (==, <, etc.).
// Wado - Correct precedence
if flags & MASK == EXPECTED { // Parsed as: (flags & MASK) == EXPECTED ✅
// ...
}
Rationale: Aligns with arithmetic operator intuition, reduces need for parentheses.
3. Use ~ for Bitwise NOT
Wado uses ~ (tilde) for bitwise NOT, not ! (Rust) or ^ (Go).
let x = 0b1010;
let y = ~x; // Bitwise NOT
Rationale:
- More familiar to developers from C/Java/Python/JavaScript backgrounds
- Clear visual distinction between logical NOT (
!) and bitwise NOT (~) - Same precedence level (unary) as Rust's
!, so no precedence issues - Compiler will catch mistakes if Rust developers use
!instead
4. No Increment/Decrement Operators
Wado does not have ++ or -- operators.
let mut count = 0;
count++; // ❌ Compile error
count += 1; // ✅ Correct
Rationale:
- Avoids undefined behavior
- Eliminates side effect confusion
- Consistent with Rust and Python
x += 1andx -= 1are clear and unambiguous
Lexer behavior: The lexer detects ++ and -- tokens and produces an error, preventing expressions like a--b or a---b from being parsed incorrectly.
5. No Power Operator
Wado does not have a ** power operator. Use the pow() function instead.
let result = x ** 2; // ❌ Compile error
let result = pow(x, 2); // ✅ Correct
Rationale:
- Wasm has no native power instruction (would compile to function call anyway)
- Avoids precedence ambiguity (Python's
-1**2 = -1is counterintuitive) - Explicit function call is clearer
- Consistent with Rust, Go, C, and Java
6. Comparison Operator Chaining: Mathematical Chaining
Wado supports mathematical comparison chaining similar to Python, but with stricter rules.
Valid chains (same direction):
a < b < c // ✅ OK: a < b AND b < c (ascending)
a > b > c // ✅ OK: a > b AND b > c (descending)
a <= b <= c // ✅ OK: a <= b AND b <= c
a >= b >= c // ✅ OK: a >= b AND b >= c
a == b == c // ✅ OK: a == b AND b == c
Invalid chains (semantic error):
a < b > c // ❌ Semantic error: mixed directions
a > b < c // ❌ Semantic error: mixed directions
a < b >= c // ❌ Semantic error: mixing < and >=
a == b < c // ❌ Semantic error: mixing == and inequality
a != b != c // ❌ Semantic error: != chaining not allowed
Chaining Rules:
- Same-direction inequality:
</<=can only chain with</<=, and>/>=can only chain with>/>= - Equality chaining:
==can only chain with== - No
!=chaining:!=cannot be chained at all - No mixing: Cannot mix equality operators with inequality operators
Rationale:
- Mathematical intuition: Range checks like
0 <= x <= 100are natural and common - Same as Python: Developers familiar with Python will find this familiar
- Clearer intent:
a < b < cis more readable thana < b && b < c - Reject ambiguous cases: Mixed directions (
a < b > c) are rarely intentional !=is ambiguous: The meaning ofa != b != cis unclear (is it "a, b, c are all different" or "a != b AND b != c"?)
Implementation: The parser allows comparison operators to be chained (left-associative). The semantic analyser validates:
- All operators in the chain are in the same "group" (ascending, descending, or equality)
!=is never chained
Consequences
Positive
- Fixes C's design mistake:
flags & MASK == VALUEworks correctly without parentheses - Avoids undefined behavior: No
++/--operators - Clear and explicit:
pow(x, y)instead of ambiguous** - Familiar to C/Java/Python developers:
~for bitwise NOT - Mathematical comparison chaining:
0 <= x <= 100works naturally like Python - Rejects ambiguous chains:
a < b > canda != b != care errors - Consistent with Rust precedence: Minimal learning curve (except
~and chaining) - Battle-tested: Rust's precedence has been proven in production
Negative
- Diverges from Rust on bitwise NOT: Rust developers might use
!by habit- Mitigation: Compiler error will catch this immediately
- Diverges from Rust on comparison chaining: Rust rejects all chaining, Wado allows valid chains
- Mitigation: Well-documented feature; clearer than Rust's blanket rejection
- Comparison chaining requires semantic analysis: Parser accepts all chains, analyser validates
- Mitigation: Clear error messages guide developers to fix invalid chains
!=chaining is rejected: Some developers might expecta != b != cto work- Mitigation: Error message suggests alternatives like
a != b && b != cor "all different" checks
- Mitigation: Error message suggests alternatives like
Precedence Table for Wado
From highest to lowest precedence:
| Level | Operators | Associativity | Description |
|---|---|---|---|
| 1 | ::, ., () |
Left-to-right | Paths, calls, fields |
| 2 | ? |
N/A | Error propagation |
| 3 | ~, -, *, &, &mut |
Right-to-left | Unary operators |
| 4 | as |
Left-to-right | Type cast |
| 5 | *, /, % |
Left-to-right | Multiplicative |
| 6 | +, - |
Left-to-right | Additive |
| 7 | <<, >> |
Left-to-right | Bitwise shift |
| 8 | & |
Left-to-right | Bitwise AND |
| 9 | ^ |
Left-to-right | Bitwise XOR |
| 10 | \| |
Left-to-right | Bitwise OR |
| 11 | matches { pattern } |
Left (self-delimiting) | Pattern test (postfix) |
| 12 | ! |
Right-to-left | Logical NOT (unary) |
| 13 | ==, <, >, <=, >= (left-assoc with rules), != (no chain) |
Restricted | Comparison |
| 14 | && |
Left-to-right | Logical AND |
| 15 | \|\| |
Left-to-right | Logical OR |
| 16 | ..<, ..= |
N/A | Range operators |
| 17 | =, +=, -=, etc. |
Right-to-left | Assignment |
Key Differences from Rust
- Level 3:
~is bitwise NOT (Rust uses!for both). Logical!is a separate operator at Level 12, looser than the other unary operators. - Level 4:
assits between unary (level 3) and multiplicative (level 5), so*x as Tparses as(*x) as T— same as Rust. - Levels 11–12:
matchesis a postfix operator and!is a unary operator, but both bind below the binary operators. Rust has neither placement (it has nomatches, and its!is a tight unary). - Level 13: Comparison chaining allowed with semantic validation:
- Same-direction chains OK:
a < b < c,a > b > c,a == b == c - Mixed-direction chains rejected:
a < b > c !=cannot be chained- Rust rejects all comparison chaining at parse level
- Same-direction chains OK:
matches (Level 11) and logical ! (Level 12)
matches binds looser than the binary operators, as, and the value-producing
unary operators, and tighter than logical !. Its { pattern } right-hand side
is self-delimiting; the scrutinee side is left-associative.
!x matches { P }is!(x matches { P })— "xdoes not matchP".*x matches { P },x as T matches { P },a + b matches { P }, andflags & MASK matches { P }need no parentheses; a comparison, range, or assignment scrutinee does ((a == b) matches { P }).!binds tighter than comparison and&&/||, so!a == bis(!a) == b.
References
- Operator precedence is broken - foonathan.net
- C Operator Precedence - cppreference.com
- Rust Reference - Expressions
- Go 101 - Common Operators
- Python Expressions Documentation
- MDN - JavaScript Operator Precedence
- Learn C++ - Increment/decrement operators and side effects
See also: docs/operator-precedence-research.md for detailed cross-language comparison.
