Operator nodes — BinaryOpNode, UnaryOpNode, TernaryNode, CastNode
The IR has three operator nodes: BinaryOpNode, UnaryOpNode, TernaryNode. They cover the vast majority of expressions in source programs. The closely-related CastNode is also documented here.
BinaryOpNode
class BinaryOpNode : public EMPLNode {
public:
std::string op;
EMPLNodePtr left;
EMPLNodePtr right;
BinaryOpNode() : EMPLNode(NodeKind::BinaryOp) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
| Field |
Type |
Source |
Description |
kind |
NodeKind::BinaryOp |
EMPLNode.h:477 |
Discriminator. |
op |
std::string |
EMPLNode.h:473 |
Operator name (see table below). Required. |
left |
EMPLNodePtr |
EMPLNode.h:474 |
Left operand. Required. |
right |
EMPLNodePtr |
EMPLNode.h:475 |
Right operand. Required. |
Operator allow-list
The validator (rule V2) accepts the following strings in op:
Arithmetic
op |
Meaning |
Source examples |
+ |
Addition |
a + b (all languages) |
- |
Subtraction |
a - b |
* |
Multiplication |
a * b |
/ |
Division |
a / b |
% |
Modulo |
a % b (JS); a % b (Python); a % b (C, C++, Java) |
** |
Exponentiation |
a ** b (JS ES2016, Python, Ruby) |
Comparison
op |
Meaning |
Notes |
== |
Equality (loose) |
Canonicalized from === if MPL_CANONICALIZE_STRICT=1 |
!= |
Inequality (loose) |
Same |
=== |
Strict equality |
Preserved in IR; canonicalizer may downgrade (see canonicalizer.md rule C2) |
!== |
Strict inequality |
Same |
< |
Less than |
|
<= |
Less than or equal |
|
> |
Greater than |
|
>= |
Greater than or equal |
|
Logical
op |
Meaning |
&& |
Logical AND |
|| |
Logical OR |
?? |
Nullish coalescing (JS, C#) |
Bitwise
op |
Meaning |
& |
Bitwise AND |
| |
Bitwise OR |
^ |
Bitwise XOR |
<< |
Left shift |
>> |
Right shift |
>>> |
Unsigned right shift (JS, Java) |
String / collection
op |
Meaning |
in |
Membership test (x in arr — JS, Python) |
instanceof |
Type test (obj instanceof Class — JS, Java) |
Examples
a + b * c → BinaryOpNode("+", a, BinaryOpNode("*", b, c))
x === y → BinaryOpNode("===", x, y)
arr.includes(k) → MethodCallNode("includes", IdentifierNode("arr"), k)
k in obj → BinaryOpNode("in", k, obj)
a ** b → BinaryOpNode("**", a, b)
x is None → BinaryOpNode("==", x, LiteralNode(null)) // canonicalized from "is"
Source-language mapping
| Source syntax |
op |
JS a + b |
+ |
JS a === b |
=== |
Python a ** b |
** |
Python a is b |
== (canonicalized) |
Python a is not b |
!= (canonicalized) |
Ruby a..b (range) |
.. (canonicalized to BinaryOpNode("..", a, b)) — kept distinct from comparison < |
C# a ?? b |
?? |
PHP $a . $b (concat) |
+ (canonicalized) — backends specializing on string concatenation may read metadata["concat"] |
SQL a AND b |
&& (canonicalized) |
Backend lowering
| Backend |
Lowered form |
| C++ |
Direct operator mapping: + → +, ** → std::pow, === / !== → mpl_strict_eq / mpl_strict_ne (see empl.h), ?? → ternary or mpl_coalesce. |
| Python (planned) |
Direct mapping; ** → **, === → ==. |
| JavaScript (planned) |
Direct; === preserved. |
UnaryOpNode
class UnaryOpNode : public EMPLNode {
public:
std::string op;
EMPLNodePtr operand;
bool isPrefix = true;
UnaryOpNode() : EMPLNode(NodeKind::UnaryOp) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
| Field |
Type |
Source |
Description |
kind |
NodeKind::UnaryOp |
EMPLNode.h:488 |
Discriminator. |
op |
std::string |
EMPLNode.h:484 |
Operator name. Required. |
operand |
EMPLNodePtr |
EMPLNode.h:485 |
The operand expression. Required. |
isPrefix |
bool |
EMPLNode.h:486 |
true for prefix (!x, ++x), false for postfix (x++). Defaults to true. |
Operator allow-list
Logical / arithmetic
op |
isPrefix |
Meaning |
! |
true |
Logical NOT |
- |
true |
Numeric negation |
+ |
true |
Numeric promotion (unary plus) |
~ |
true |
Bitwise NOT |
Increment / decrement
op |
isPrefix |
Meaning |
++ |
true / false |
Pre/post increment |
-- |
true / false |
Pre/post decrement |
Type queries
op |
isPrefix |
Meaning |
typeof |
true |
Type-of query (JS, TS) |
void |
true |
void operator (JS) — always returns undefined |
delete |
true |
Property deletion (JS) |
Examples
!x → UnaryOpNode("!", x, true)
typeof x → UnaryOpNode("typeof", x, true)
delete o.p → UnaryOpNode("delete", MemberAccessNode(o, "p"), true)
i++ → UnaryOpNode("++", i, false)
++i → UnaryOpNode("++", i, true)
-x → UnaryOpNode("-", x, true)
Source-language mapping
| Source |
op |
JS !x |
! |
JS typeof x |
typeof |
JS void 0 |
void |
C++ *p (dereference) |
NOT a unary op — represented as a MemberAccessNode with metadata["deref"]="true", OR a dedicated DereferenceNode (planned) |
Python -x |
- |
Python not x |
! (canonicalized) |
Ruby !x |
! |
TernaryNode
class TernaryNode : public EMPLNode {
public:
EMPLNodePtr condition;
EMPLNodePtr whenTrue;
EMPLNodePtr whenFalse;
TernaryNode() : EMPLNode(NodeKind::Ternary) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
| Field |
Type |
Source |
Description |
kind |
NodeKind::Ternary |
EMPLNode.h:616 |
Discriminator. |
condition |
EMPLNodePtr |
EMPLNode.h:612 |
Test expression. Required. |
whenTrue |
EMPLNodePtr |
EMPLNode.h:613 |
Expression evaluated when condition is truthy. Required. |
whenFalse |
EMPLNodePtr |
EMPLNode.h:614 |
Expression evaluated when condition is falsy. Required. |
Source mapping
| Source |
Field config |
c ? a : b (JS / C / Java / etc.) |
condition=c, whenTrue=a, whenFalse=b |
a if c else b (Python) |
condition=c, whenTrue=a, whenFalse=b (canonicalized) |
Backend lowering
Direct C++ ternary: c ? a : b. The backend does NOT desugar ternaries to if-else (it preserves source semantics).
CastNode
class CastNode : public EMPLNode {
public:
std::string targetType;
EMPLNodePtr expression;
CastNode() : EMPLNode(NodeKind::Cast) {}
void accept(EMPLVisitor& v) override { v.visit(*this); }
};
| Field |
Type |
Source |
Description |
kind |
NodeKind::Cast |
EMPLNode.h:605 |
Discriminator. |
targetType |
std::string |
EMPLNode.h:602 |
Source-text type expression (e.g. "int", "Promise<string>", "MyClass"). Required. |
expression |
EMPLNodePtr |
EMPLNode.h:603 |
The expression being cast. Required. |
Source mapping
| Source |
Field config |
(x as number) (TS) |
targetType="number", expression=x |
int(x) (Python) |
targetType="int", expression=x |
(int)x (C / C++ / Java / C#) |
targetType="int", expression=x |
x as MyClass (Rust) |
targetType="MyClass", expression=x |
Backend lowering
| Backend |
Lowered form |
| C++ |
static_cast<targetType>(expr) (or reinterpret_cast / const_cast if metadata["cast_kind"] is set) |
| Python (planned) |
targetType(expr) |
| JavaScript (planned) |
Direct — JS has no real casts |
Validation rules
| Rule |
Constraint |
| V2 |
BinaryOpNode::op must be in the allow-list above. |
| V2 |
UnaryOpNode::op must be in the allow-list above. |
| V1 |
All required fields are non-null. |
See also