1.3 Expressions
Operator Precedence
udewy parses expressions using the precedence table below. Binary operators are left-associative within a precedence level, and parentheses can be used to override the default grouping.
Operand evaluation remains left-to-right, even when precedence groups the expression differently. For example, a + b * c evaluates a, then b, then c, and groups as a + (b * c).
Precedence levels (highest to lowest):
| Level | Operators | Description |
|---|---|---|
| 7 | *, //, % | Multiplicative |
| 6 | +, - | Additive |
| 5 | <<, >> | Shift |
| 4 | =?, not=?, >?, <?, >=?, <=? | Comparison |
| 3 | and | Bitwise/logical AND |
| 2 | xor | Bitwise/logical XOR |
| 1 | or | Bitwise/logical OR |
Examples:
# OK: multiplicative operators bind before additive operators
let product_sum:int = a + b * c
# OK: explicit grouping
let grouped:int = (a + b) * c
# OK: left-associative within the same precedence level
let chain:int = a + b + c
Unary Operators
| Operator | Description | Semantics |
|---|---|---|
- | Negation | Two's complement negation: 0 - x |
not | Bitwise/logical NOT | Inverts all 64 bits |
Unary operators bind tightly to their operand:
let x:int = -(a + b)
let y:int = not flags
Arithmetic Operators
| Operator | Description | Semantics |
|---|---|---|
+ | Addition | 64-bit wrapping addition |
- | Subtraction | 64-bit wrapping subtraction |
* | Multiplication | 64-bit wrapping multiplication |
// | Integer division | Signed 64-bit division, truncated toward zero |
% | Modulo | Signed 64-bit remainder |
Division and modulo use signed interpretation (RISC-V div / rem semantics on all targets):
| Case | a // b | a % b |
|---|---|---|
b = 0 | -1 (0xFFFF_FFFF_FFFF_FFFF) | a |
INT_MIN // -1 | INT_MIN | 0 |
All other cases use truncating signed division toward zero.
Shift Operators
| Operator | Description | Semantics |
|---|---|---|
<< | Left shift | Shift left, fill with zeros |
>> | Right shift (unsigned) | Logical shift right, fill with zeros |
The shift amount is masked to the low 6 bits (0-63).
Important: The >> operator performs an unsigned (logical) shift, filling vacated bits with zeros regardless of the sign bit. For arithmetic (signed) right shift that preserves the sign bit, use the __signed_shr__ intrinsic. See Semantic Differences
Comparison Operators
All comparison operators return true (0xFFFF_FFFF_FFFF_FFFF) or false (0x0000_0000_0000_0000).
| Operator | Description | Semantics |
|---|---|---|
=? | Equal | True if operands are bit-identical |
not=? | Not equal | True if operands differ in any bit |
>? | Greater than | Signed comparison |
<? | Less than | Signed comparison |
>=? | Greater or equal | Signed comparison |
<=? | Less or equal | Signed comparison |
Note: Relational comparisons (>?, <?, etc.) interpret operands as signed 64-bit integers.
Bitwise/Logical Operators
| Operator | Description | Semantics |
|---|---|---|
and | Bitwise AND | 64-bit bitwise AND |
or | Bitwise OR | 64-bit bitwise OR |
xor | Bitwise XOR | 64-bit bitwise XOR |
Due to the boolean representation (true = all 1s, false = all 0s), these operators work correctly as both bitwise and logical operators.
Important: In ordinary expressions, and and or are bitwise and both operands are always evaluated. In if and loop conditions only, and and or use logical short-circuit evaluation (compatible with Dewy bool conditions): the right-hand side is skipped when the result is already determined.
Parentheses and Grouping
Parentheses override precedence and grouping:
let x:int = (a + b) * c
Function Calls
Named call:
result = add(1 2)
Expression call (calling a computed function pointer):
let fn_ptr:int = get_handler()
(@fn_ptr)(arg1 arg2)
Arguments are space-separated (no commas).
NOTE: Indirect calls require parentheses around the callee. Bare
fn_ptr(arg1 arg2)is always a named call: it looks up a top-level functionfn_ptrand ignores any local or global binding of that name. Use(@fn_ptr)(arg1 arg2)to call through a value. Consequently, binding a local or global with the same name as an existing top-level function is ill-formed —name(...)will still call the function, not the binding.
@nameis decorative in udewy: it is the value ofname, exactly as the bare name is. It exists so a udewy program reads the same under Dewy, where a bare function name is always a call and@namemeans the function itself. Write it wherever a function is used as a value —return @double,let handler:int = @on_start— and it is required when a name in parentheses is called:(fn_ptr)(args)is rejected, because under Dewy that would callfn_ptr()first and apply the arguments to its result.