Skip to content

Commit 9e3efbb

Browse files
committed
parser/token:
- support exponentiation (**) and numeric separators; - eval: implement **/**= with BigInt support; - add tests
1 parent a05d3b5 commit 9e3efbb

File tree

5 files changed

+193
-14
lines changed

5 files changed

+193
-14
lines changed

src/core.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub enum Expr {
143143
NullishAssign(Box<Expr>, Box<Expr>), // target, value
144144
AddAssign(Box<Expr>, Box<Expr>), // target, value
145145
SubAssign(Box<Expr>, Box<Expr>), // target, value
146+
PowAssign(Box<Expr>, Box<Expr>), // target, value
146147
MulAssign(Box<Expr>, Box<Expr>), // target, value
147148
DivAssign(Box<Expr>, Box<Expr>), // target, value
148149
ModAssign(Box<Expr>, Box<Expr>), // target, value
@@ -199,6 +200,7 @@ pub enum BinaryOp {
199200
InstanceOf,
200201
In,
201202
NullishCoalescing,
203+
Pow,
202204
}
203205

204206
#[derive(Debug, Clone)]

src/core/eval.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,7 @@ pub fn evaluate_expr(env: &JSObjectDataPtr, expr: &Expr) -> Result<Value, JSErro
17181718
Expr::AddAssign(target, value) => evaluate_add_assign(env, target, value),
17191719
Expr::SubAssign(target, value) => evaluate_sub_assign(env, target, value),
17201720
Expr::MulAssign(target, value) => evaluate_mul_assign(env, target, value),
1721+
Expr::PowAssign(target, value) => evaluate_pow_assign(env, target, value),
17211722
Expr::DivAssign(target, value) => evaluate_div_assign(env, target, value),
17221723
Expr::ModAssign(target, value) => evaluate_mod_assign(env, target, value),
17231724
Expr::Increment(expr) => evaluate_increment(env, expr),
@@ -2035,6 +2036,67 @@ fn evaluate_mul_assign(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Re
20352036
Ok(result)
20362037
}
20372038

2039+
fn evaluate_pow_assign(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Result<Value, JSError> {
2040+
// a **= b is equivalent to a = a ** b
2041+
let left_val = evaluate_expr(env, target)?;
2042+
let right_val = evaluate_expr(env, value)?;
2043+
let result = match (left_val, right_val) {
2044+
(Value::Number(ln), Value::Number(rn)) => Value::Number(ln.powf(rn)),
2045+
(Value::BigInt(la), Value::BigInt(rb)) => {
2046+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(JSError::EvaluationError {
2047+
message: "invalid bigint".to_string(),
2048+
})?;
2049+
let b = BigInt::parse_bytes(rb.as_bytes(), 10).ok_or(JSError::EvaluationError {
2050+
message: "invalid bigint".to_string(),
2051+
})?;
2052+
if b < BigInt::from(0) {
2053+
return Err(JSError::EvaluationError {
2054+
message: "negative exponent for bigint".to_string(),
2055+
});
2056+
}
2057+
let exp = b.to_u32().ok_or(JSError::EvaluationError {
2058+
message: "exponent too large".to_string(),
2059+
})?;
2060+
Value::BigInt(a.pow(exp).to_string())
2061+
}
2062+
(Value::BigInt(la), Value::Number(rn)) => {
2063+
if rn < 0.0 || rn.fract() != 0.0 {
2064+
return Err(JSError::EvaluationError {
2065+
message: "invalid exponent for bigint".to_string(),
2066+
});
2067+
}
2068+
let exp_u = rn as u32;
2069+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(JSError::EvaluationError {
2070+
message: "invalid bigint".to_string(),
2071+
})?;
2072+
Value::BigInt(a.pow(exp_u).to_string())
2073+
}
2074+
(Value::Number(ln), Value::BigInt(rb)) => {
2075+
if let Some(rv) = BigInt::parse_bytes(rb.as_bytes(), 10).and_then(|bi| bi.to_f64()) {
2076+
Value::Number(ln.powf(rv))
2077+
} else {
2078+
return Err(JSError::EvaluationError {
2079+
message: "invalid exponent".to_string(),
2080+
});
2081+
}
2082+
}
2083+
_ => {
2084+
return Err(JSError::EvaluationError {
2085+
message: "Invalid operands for **=".to_string(),
2086+
});
2087+
}
2088+
};
2089+
2090+
// update assignment target
2091+
let assignment_expr = match &result {
2092+
Value::Number(n) => Expr::Number(*n),
2093+
Value::BigInt(s) => Expr::BigInt(s.clone()),
2094+
_ => unreachable!(),
2095+
};
2096+
evaluate_assignment_expr(env, target, &assignment_expr)?;
2097+
Ok(result)
2098+
}
2099+
20382100
fn evaluate_div_assign(env: &JSObjectDataPtr, target: &Expr, value: &Expr) -> Result<Value, JSError> {
20392101
// a /= b is equivalent to a = a / b
20402102
let left_val = evaluate_expr(env, target)?;
@@ -2565,6 +2627,52 @@ fn evaluate_binary(env: &JSObjectDataPtr, left: &Expr, op: &BinaryOp, right: &Ex
25652627
message: "error".to_string(),
25662628
}),
25672629
},
2630+
BinaryOp::Pow => match (l, r) {
2631+
(Value::Number(ln), Value::Number(rn)) => Ok(Value::Number(ln.powf(rn))),
2632+
(Value::BigInt(la), Value::BigInt(rb)) => {
2633+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(JSError::EvaluationError {
2634+
message: "invalid bigint".to_string(),
2635+
})?;
2636+
let b = BigInt::parse_bytes(rb.as_bytes(), 10).ok_or(JSError::EvaluationError {
2637+
message: "invalid bigint".to_string(),
2638+
})?;
2639+
// exponent must be non-negative and fit into u32 for pow
2640+
if b < BigInt::from(0) {
2641+
return Err(JSError::EvaluationError {
2642+
message: "negative exponent for bigint".to_string(),
2643+
});
2644+
}
2645+
let exp = b.to_u32().ok_or(JSError::EvaluationError {
2646+
message: "exponent too large".to_string(),
2647+
})?;
2648+
Ok(Value::BigInt(a.pow(exp).to_string()))
2649+
}
2650+
(Value::BigInt(la), Value::Number(rn)) => {
2651+
if rn < 0.0 || rn.fract() != 0.0 {
2652+
return Err(JSError::EvaluationError {
2653+
message: "invalid exponent for bigint".to_string(),
2654+
});
2655+
}
2656+
let exp_u = rn as u32;
2657+
let a = BigInt::parse_bytes(la.as_bytes(), 10).ok_or(JSError::EvaluationError {
2658+
message: "invalid bigint".to_string(),
2659+
})?;
2660+
Ok(Value::BigInt(a.pow(exp_u).to_string()))
2661+
}
2662+
// number ** bigint -> try converting bigint to number if possible
2663+
(Value::Number(ln), Value::BigInt(rb)) => {
2664+
if let Some(rv) = BigInt::parse_bytes(rb.as_bytes(), 10).and_then(|bi| bi.to_f64()) {
2665+
Ok(Value::Number(ln.powf(rv)))
2666+
} else {
2667+
Err(JSError::EvaluationError {
2668+
message: "invalid exponent".to_string(),
2669+
})
2670+
}
2671+
}
2672+
_ => Err(JSError::EvaluationError {
2673+
message: "error".to_string(),
2674+
}),
2675+
},
25682676
BinaryOp::Div => match (l, r) {
25692677
(Value::Number(ln), Value::Number(rn)) => {
25702678
if rn == 0.0 {

src/core/parser.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ pub fn parse_assignment(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
111111
let right = parse_assignment(tokens)?;
112112
Ok(Expr::SubAssign(Box::new(left), Box::new(right)))
113113
}
114+
Token::PowAssign => {
115+
tokens.remove(0);
116+
let right = parse_assignment(tokens)?;
117+
Ok(Expr::PowAssign(Box::new(left), Box::new(right)))
118+
}
114119
Token::MulAssign => {
115120
tokens.remove(0);
116121
let right = parse_assignment(tokens)?;
@@ -253,7 +258,7 @@ fn parse_additive(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
253258
}
254259

255260
fn parse_multiplicative(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
256-
let left = parse_primary(tokens)?;
261+
let left = parse_exponentiation(tokens)?;
257262
if tokens.is_empty() {
258263
return Ok(left);
259264
}
@@ -277,6 +282,21 @@ fn parse_multiplicative(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
277282
}
278283
}
279284

285+
fn parse_exponentiation(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
286+
// Right-associative exponentiation operator: a ** b ** c -> a ** (b ** c)
287+
let left = parse_primary(tokens)?;
288+
if tokens.is_empty() {
289+
return Ok(left);
290+
}
291+
if matches!(tokens[0], Token::Exponent) {
292+
tokens.remove(0);
293+
let right = parse_exponentiation(tokens)?; // right-associative
294+
Ok(Expr::Binary(Box::new(left), BinaryOp::Pow, Box::new(right)))
295+
} else {
296+
Ok(left)
297+
}
298+
}
299+
280300
fn parse_primary(tokens: &mut Vec<Token>) -> Result<Expr, JSError> {
281301
// Skip any leading line terminators inside expressions so multi-line
282302
// expression continuations like `a +\n b` parse correctly.

src/core/token.rs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub enum Token {
1111
Plus,
1212
Minus,
1313
Multiply,
14+
/// Exponentiation operator `**`
15+
Exponent,
1416
Divide,
1517
/// Regex literal with pattern and flags (e.g. /pattern/flags)
1618
Regex(String, String),
@@ -87,6 +89,8 @@ pub enum Token {
8789
Async,
8890
Await,
8991
LineTerminator,
92+
/// Exponentiation assignment (`**=`)
93+
PowAssign,
9094
}
9195

9296
impl Token {
@@ -175,7 +179,14 @@ pub fn tokenize(expr: &str) -> Result<Vec<Token>, JSError> {
175179
}
176180
}
177181
'*' => {
178-
if i + 1 < chars.len() && chars[i + 1] == '=' {
182+
// Handle exponentiation '**' and '**=' first, then '*='
183+
if i + 2 < chars.len() && chars[i + 1] == '*' && chars[i + 2] == '=' {
184+
tokens.push(Token::PowAssign);
185+
i += 3;
186+
} else if i + 1 < chars.len() && chars[i + 1] == '*' {
187+
tokens.push(Token::Exponent);
188+
i += 2;
189+
} else if i + 1 < chars.len() && chars[i + 1] == '=' {
179190
tokens.push(Token::MulAssign);
180191
i += 2;
181192
} else {
@@ -424,14 +435,18 @@ pub fn tokenize(expr: &str) -> Result<Vec<Token>, JSError> {
424435
}
425436
'0'..='9' => {
426437
let start = i;
427-
// integer part
428-
while i < chars.len() && chars[i].is_ascii_digit() {
438+
// integer part (allow underscores as numeric separators)
439+
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '_') {
429440
i += 1;
430441
}
431442

432-
// BigInt literal: digits followed by 'n' (no decimal/exponent allowed)
443+
// BigInt literal: digits (possibly with underscores) followed by 'n' (no decimal/exponent allowed)
433444
if i < chars.len() && chars[i] == 'n' {
434-
let num_str: String = chars[start..i].iter().collect();
445+
let mut num_str: String = chars[start..i].iter().collect();
446+
num_str.retain(|c| c != '_');
447+
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
448+
return Err(JSError::TokenizationError);
449+
}
435450
tokens.push(Token::BigInt(num_str));
436451
i += 1; // consume trailing 'n'
437452
continue;
@@ -440,7 +455,7 @@ pub fn tokenize(expr: &str) -> Result<Vec<Token>, JSError> {
440455
// fractional part
441456
if i < chars.len() && chars[i] == '.' {
442457
i += 1;
443-
while i < chars.len() && chars[i].is_ascii_digit() {
458+
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '_') {
444459
i += 1;
445460
}
446461
}
@@ -452,20 +467,24 @@ pub fn tokenize(expr: &str) -> Result<Vec<Token>, JSError> {
452467
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
453468
j += 1;
454469
}
455-
// require at least one digit in exponent
456-
if j >= chars.len() || !chars[j].is_ascii_digit() {
470+
// require at least one digit in exponent (underscores allowed inside digits)
471+
if j >= chars.len() || !(chars[j].is_ascii_digit()) {
457472
return Err(JSError::TokenizationError);
458473
}
459-
// consume exponent digits
460-
while j < chars.len() && chars[j].is_ascii_digit() {
474+
while j < chars.len() && (chars[j].is_ascii_digit() || chars[j] == '_') {
461475
j += 1;
462476
}
463477
i = j;
464478
}
465479

466-
let num_str: String = chars[start..i].iter().collect();
467-
let num = num_str.parse::<f64>().map_err(|_| JSError::TokenizationError)?;
468-
tokens.push(Token::Number(num));
480+
// Build numeric string and remove numeric separators
481+
let mut num_str: String = chars[start..i].iter().collect();
482+
num_str.retain(|c| c != '_');
483+
// Convert to f64
484+
match num_str.parse::<f64>() {
485+
Ok(n) => tokens.push(Token::Number(n)),
486+
Err(_) => return Err(JSError::TokenizationError),
487+
}
469488
}
470489
'"' => {
471490
i += 1; // skip opening quote

tests/parser_and_tokenization.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,33 @@ fn trailing_comma_and_newline_before_rbrace_is_allowed() {
5555
// and the tokens left should be empty
5656
assert!(tokens.is_empty());
5757
}
58+
59+
#[test]
60+
fn exponentiation_and_numeric_separators_supported() {
61+
// Exponentiation for numbers
62+
let res = evaluate_script("2 ** 3;");
63+
match res {
64+
Ok(crate::Value::Number(n)) => assert_eq!(n, 8.0),
65+
_ => panic!("expected numeric result for 2 ** 3"),
66+
}
67+
68+
let res2 = evaluate_script("2 ** 3 ** 2;");
69+
match res2 {
70+
Ok(crate::Value::Number(n)) => assert_eq!(n, 512.0),
71+
_ => panic!("expected numeric result for 2 ** 3 ** 2"),
72+
}
73+
74+
// Numeric separators
75+
let res3 = evaluate_script("1_000_000 + 2000;");
76+
match res3 {
77+
Ok(crate::Value::Number(n)) => assert_eq!(n, 1_002_000.0),
78+
_ => panic!("expected numeric result for 1_000_000 + 2000"),
79+
}
80+
81+
// BigInt with separators and exponentiation
82+
let res4 = evaluate_script("1_000n ** 2n;");
83+
match res4 {
84+
Ok(crate::Value::BigInt(s)) => assert_eq!(s, "1000000".to_string()),
85+
_ => panic!("expected bigint result for 1_000n ** 2n"),
86+
}
87+
}

0 commit comments

Comments
 (0)