Skip to content

Commit 09e9923

Browse files
committed
Add XOR (^) and XOR assignment (^=) support:
- Tokenizer: Added Token::BitXor and Token::BitXorAssign. - Parser: Added parse_bitwise_xor and integrated it into precedence. - AST: Added Expr::BitXor and Expr::BitXorAssign. - Evaluator: Implemented BinaryOp::BitXor and evaluate_bitxor_assign. - Tests: Added unit tests for XOR and XOR assignment.
1 parent bb35544 commit 09e9923

File tree

5 files changed

+152
-1
lines changed

5 files changed

+152
-1
lines changed

src/core.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub enum Expr {
139139
MulAssign(Box<Expr>, Box<Expr>), // target, value
140140
DivAssign(Box<Expr>, Box<Expr>), // target, value
141141
ModAssign(Box<Expr>, Box<Expr>), // target, value
142+
BitXorAssign(Box<Expr>, Box<Expr>), // target, value
142143
Increment(Box<Expr>),
143144
Decrement(Box<Expr>),
144145
PostIncrement(Box<Expr>),
@@ -172,6 +173,7 @@ pub enum Expr {
172173
/// Logical operators with short-circuit semantics
173174
LogicalAnd(Box<Expr>, Box<Expr>),
174175
LogicalOr(Box<Expr>, Box<Expr>),
176+
BitXor(Box<Expr>, Box<Expr>),
175177
Value(Value), // literal value
176178
}
177179

@@ -194,6 +196,7 @@ pub enum BinaryOp {
194196
In,
195197
NullishCoalescing,
196198
Pow,
199+
BitXor,
197200
}
198201

199202
#[derive(Debug, Clone)]

src/core/eval.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,7 @@ pub fn evaluate_expr(env: &JSObjectDataPtr, expr: &Expr) -> Result<Value, JSErro
16981698
Expr::PowAssign(target, value) => evaluate_pow_assign(env, target, value),
16991699
Expr::DivAssign(target, value) => evaluate_div_assign(env, target, value),
17001700
Expr::ModAssign(target, value) => evaluate_mod_assign(env, target, value),
1701+
Expr::BitXorAssign(target, value) => evaluate_bitxor_assign(env, target, value),
17011702
Expr::Increment(expr) => evaluate_increment(env, expr),
17021703
Expr::Decrement(expr) => evaluate_decrement(env, expr),
17031704
Expr::PostIncrement(expr) => evaluate_post_increment(env, expr),
@@ -1719,6 +1720,7 @@ pub fn evaluate_expr(env: &JSObjectDataPtr, expr: &Expr) -> Result<Value, JSErro
17191720
let l = evaluate_expr(env, left)?;
17201721
if is_truthy(&l) { Ok(l) } else { evaluate_expr(env, right) }
17211722
}
1723+
Expr::BitXor(left, right) => evaluate_binary(env, left, &crate::core::BinaryOp::BitXor, right),
17221724
Expr::Index(obj, idx) => evaluate_index(env, obj, idx),
17231725
Expr::Property(obj, prop) => evaluate_property(env, obj, prop),
17241726
Expr::Call(func_expr, args) => match evaluate_call(env, func_expr, args) {
@@ -2237,6 +2239,52 @@ fn evaluate_mod_assign(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Re
22372239
Ok(result)
22382240
}
22392241

2242+
fn evaluate_bitxor_assign(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Result<Value, JSError> {
2243+
// a ^= b is equivalent to a = a ^ b
2244+
let left_val = evaluate_expr(env, target)?;
2245+
let right_val = evaluate_expr(env, value)?;
2246+
let result = match (left_val, right_val) {
2247+
(Value::Number(ln), Value::Number(rn)) => {
2248+
let to_int32 = |n: f64| -> i32 {
2249+
if !n.is_finite() || n == 0.0 {
2250+
return 0;
2251+
}
2252+
let two32 = 4294967296f64;
2253+
let int = n.trunc();
2254+
let int32bit = ((int % two32) + two32) % two32;
2255+
if int32bit >= 2147483648f64 {
2256+
(int32bit - two32) as i32
2257+
} else {
2258+
int32bit as i32
2259+
}
2260+
};
2261+
Value::Number((to_int32(ln) ^ to_int32(rn)) as f64)
2262+
}
2263+
(Value::BigInt(la), Value::BigInt(rb)) => {
2264+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(raise_eval_error!("invalid bigint"))?;
2265+
let b = BigInt::parse_bytes(rb.as_bytes(), 10).ok_or(raise_eval_error!("invalid bigint"))?;
2266+
use std::ops::BitXor;
2267+
Value::BigInt(a.bitxor(&b).to_string())
2268+
}
2269+
(Value::BigInt(_), Value::Number(_)) | (Value::Number(_), Value::BigInt(_)) => {
2270+
return Err(raise_type_error!("Cannot mix BigInt and other types"));
2271+
}
2272+
_ => {
2273+
return Err(raise_eval_error!("Invalid operands for ^="));
2274+
}
2275+
};
2276+
match &result {
2277+
Value::Number(n) => {
2278+
let _ = evaluate_assignment_expr(env, target, &Expr::Number(*n))?;
2279+
}
2280+
Value::BigInt(s) => {
2281+
let _ = evaluate_assignment_expr(env, target, &Expr::BigInt(s.clone()))?;
2282+
}
2283+
_ => unreachable!(),
2284+
}
2285+
Ok(result)
2286+
}
2287+
22402288
fn evaluate_assignment_expr(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Result<Value, JSError> {
22412289
let val = evaluate_expr(env, value)?;
22422290
match target {
@@ -3274,6 +3322,38 @@ fn evaluate_binary(env: &JSObjectDataPtr, left: &Expr, op: &BinaryOp, right: &Ex
32743322
_ => Ok(Value::Boolean(false)),
32753323
}
32763324
}
3325+
BinaryOp::BitXor => match (l, r) {
3326+
(Value::Number(ln), Value::Number(rn)) => {
3327+
// ToInt32 semantics for Number inputs
3328+
let to_int32 = |n: f64| -> i32 {
3329+
if !n.is_finite() || n == 0.0 {
3330+
return 0;
3331+
}
3332+
let two32 = 4294967296f64;
3333+
let int = n.trunc();
3334+
// modulo 2^32
3335+
let int32bit = ((int % two32) + two32) % two32;
3336+
if int32bit >= 2147483648f64 {
3337+
(int32bit - two32) as i32
3338+
} else {
3339+
int32bit as i32
3340+
}
3341+
};
3342+
let a = to_int32(ln);
3343+
let b = to_int32(rn);
3344+
Ok(Value::Number((a ^ b) as f64))
3345+
}
3346+
(Value::BigInt(la), Value::BigInt(rb)) => {
3347+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(raise_eval_error!("invalid bigint"))?;
3348+
let b = BigInt::parse_bytes(rb.as_bytes(), 10).ok_or(raise_eval_error!("invalid bigint"))?;
3349+
use std::ops::BitXor;
3350+
Ok(Value::BigInt((a.bitxor(&b)).to_string()))
3351+
}
3352+
(Value::BigInt(_), Value::Number(_)) | (Value::Number(_), Value::BigInt(_)) => {
3353+
Err(raise_type_error!("Cannot mix BigInt and other types"))
3354+
}
3355+
_ => Err(raise_eval_error!("Bitwise XOR only supported for numbers or BigInt")),
3356+
},
32773357
BinaryOp::In => {
32783358
// Check if property exists in object
32793359
match (l, r) {

src/core/parser.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ pub fn parse_assignment(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
9999
Token::Assign
100100
| Token::LogicalAndAssign
101101
| Token::LogicalOrAssign
102+
| Token::BitXorAssign
102103
| Token::NullishAssign
103104
| Token::AddAssign
104105
| Token::SubAssign
@@ -165,10 +166,29 @@ pub fn parse_assignment(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
165166
let right = parse_assignment(tokens)?;
166167
Ok(Expr::ModAssign(Box::new(left), Box::new(right)))
167168
}
169+
Token::BitXorAssign => {
170+
tokens.remove(0);
171+
let right = parse_assignment(tokens)?;
172+
Ok(Expr::BitXorAssign(Box::new(left), Box::new(right)))
173+
}
168174
_ => Ok(left),
169175
}
170176
}
171177

178+
fn parse_bitwise_xor(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
179+
let left = parse_additive(tokens)?;
180+
if tokens.is_empty() {
181+
return Ok(left);
182+
}
183+
if matches!(tokens[0], Token::BitXor) {
184+
tokens.remove(0);
185+
let right = parse_bitwise_xor(tokens)?;
186+
Ok(Expr::BitXor(Box::new(left), Box::new(right)))
187+
} else {
188+
Ok(left)
189+
}
190+
}
191+
172192
fn parse_logical_and(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
173193
let left = parse_comparison(tokens)?;
174194
if tokens.is_empty() {
@@ -212,7 +232,7 @@ fn parse_nullish(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
212232
}
213233

214234
fn parse_comparison(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
215-
let left = parse_additive(tokens)?;
235+
let left = parse_bitwise_xor(tokens)?;
216236
if tokens.is_empty() {
217237
return Ok(left);
218238
}

src/core/token.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ pub enum Token {
7676
LogicalNot,
7777
LogicalAnd,
7878
LogicalOr,
79+
BitXor,
7980
LogicalAndAssign,
8081
LogicalOrAssign,
82+
BitXorAssign,
8183
NullishAssign,
8284
AddAssign,
8385
SubAssign,
@@ -433,6 +435,16 @@ pub fn tokenize(expr: &str) -> Result<Vec<Token>, JSError> {
433435
return Err(raise_tokenize_error!());
434436
}
435437
}
438+
'^' => {
439+
// Recognize '^=' (bitwise XOR assignment) and '^' (bitwise XOR)
440+
if i + 1 < chars.len() && chars[i + 1] == '=' {
441+
tokens.push(Token::BitXorAssign);
442+
i += 2;
443+
} else {
444+
tokens.push(Token::BitXor);
445+
i += 1;
446+
}
447+
}
436448
'0'..='9' => {
437449
let start = i;
438450
// integer part (allow underscores as numeric separators)

tests/number_tests.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,40 @@ mod number_tests {
465465
}
466466
}
467467
}
468+
469+
#[test]
470+
fn test_bitwise_xor_numbers() {
471+
let script = "5 ^ 3";
472+
let result = evaluate_script(script);
473+
match result {
474+
Ok(Value::Number(n)) => {
475+
assert_eq!(n, 6.0); // 5 ^ 3 = 6
476+
}
477+
_ => panic!("Expected 5 ^ 3 to evaluate to 6, got {:?}", result),
478+
}
479+
}
480+
481+
#[test]
482+
fn test_bitwise_xor_negative_numbers() {
483+
let script = "-5 ^ 3";
484+
let result = evaluate_script(script);
485+
match result {
486+
Ok(Value::Number(n)) => {
487+
assert_eq!(n, -8.0); // -5 ^ 3 = -8
488+
}
489+
_ => panic!("Expected -5 ^ 3 to evaluate to -8, got {:?}", result),
490+
}
491+
}
492+
493+
#[test]
494+
fn test_bitwise_xor_assignment() {
495+
let script = "let a = 5; a ^= 3; a";
496+
let result = evaluate_script(script);
497+
match result {
498+
Ok(Value::Number(n)) => {
499+
assert_eq!(n, 6.0); // a = 5; a ^= 3; a = 6
500+
}
501+
_ => panic!("Expected a ^= 3 to evaluate to 6, got {:?}", result),
502+
}
503+
}
468504
}

0 commit comments

Comments
 (0)