Skip to content

Commit 819a614

Browse files
feat: add ** exponentiation operator and **= augmented assignment
Adds right-associative `**` (power) operator and `**=` desugar to all pipeline stages: lexer/parser → AST → interpreter (int.pow + f64.powf with negative-exponent float promotion) → canonical/formatter/code_intel → compiler (unsupported stub) → JS emitter (Math.pow). 15/15 parser-extras tests pass, 1357/1357 full suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2278a82 commit 819a614

9 files changed

Lines changed: 94 additions & 8 deletions

File tree

examples/tests/test_parser_extras.omc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,31 @@ fn test_call_expr_array_index() {
120120
assert_eq(ops[0](10), 11, "arr[0](arg)");
121121
assert_eq(ops[2](10), 13, "arr[2](arg)");
122122
}
123+
124+
# ---- exponentiation operator ** and **= ----
125+
126+
fn test_power_int() {
127+
assert_eq(2 ** 10, 1024, "2**10");
128+
assert_eq(3 ** 0, 1, "3**0");
129+
assert_eq(3 ** 1, 3, "3**1");
130+
assert_eq(5 ** 3, 125, "5**3");
131+
}
132+
133+
fn test_power_float() {
134+
assert_eq(4.0 ** 0.5, 2.0, "4.0**0.5 = sqrt(4)");
135+
assert_eq(9.0 ** 0.5, 3.0, "9.0**0.5 = sqrt(9)");
136+
}
137+
138+
fn test_power_right_assoc() {
139+
# 2 ** 3 ** 2 = 2 ** 9 = 512 (right-associative)
140+
assert_eq(2 ** 3 ** 2, 512, "2**3**2 right-assoc");
141+
}
142+
143+
fn test_power_aug_assign() {
144+
h x = 3;
145+
x **= 3;
146+
assert_eq(x, 27, "x **= 3");
147+
h y = 2;
148+
y **= 8;
149+
assert_eq(y, 256, "y **= 8");
150+
}

omnimcode-core/src/ast.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ pub enum Expression {
235235
Mul(Box<Expression>, Box<Expression>),
236236
Div(Box<Expression>, Box<Expression>),
237237
Mod(Box<Expression>, Box<Expression>),
238+
Power(Box<Expression>, Box<Expression>),
238239

239240
// Comparisons
240241
Eq(Box<Expression>, Box<Expression>),

omnimcode-core/src/canonical.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ fn rename_expr(expr: &Expression, scope: &Scope) -> Expression {
357357
Box::new(rename_expr(a, scope)),
358358
Box::new(rename_expr(b, scope)),
359359
),
360+
Expression::Power(a, b) => Expression::Power(
361+
Box::new(rename_expr(a, scope)),
362+
Box::new(rename_expr(b, scope)),
363+
),
360364
Expression::Eq(a, b) => Expression::Eq(Box::new(rename_expr(a, scope)), Box::new(rename_expr(b, scope))),
361365
Expression::Ne(a, b) => Expression::Ne(Box::new(rename_expr(a, scope)), Box::new(rename_expr(b, scope))),
362366
Expression::Lt(a, b) => Expression::Lt(Box::new(rename_expr(a, scope)), Box::new(rename_expr(b, scope))),

omnimcode-core/src/code_intel.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ fn collect_expr_calls(e: &Expression, out: &mut BTreeSet<String>) {
189189
Expression::Dict(pairs) => for (k, v) in pairs { collect_expr_calls(k, out); collect_expr_calls(v, out); }
190190
Expression::Index { index, .. } => collect_expr_calls(index, out),
191191
Expression::ChainedIndex { object, index } => { collect_expr_calls(object, out); collect_expr_calls(index, out); }
192-
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b)
192+
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b) | Expression::Power(a, b)
193193
| Expression::Eq(a, b) | Expression::Ne(a, b) | Expression::Lt(a, b) | Expression::Le(a, b) | Expression::Gt(a, b) | Expression::Ge(a, b)
194194
| Expression::And(a, b) | Expression::Or(a, b)
195195
| Expression::BitAnd(a, b) | Expression::BitOr(a, b) | Expression::BitXor(a, b)
@@ -292,7 +292,7 @@ pub fn ast_size(source: &str) -> Result<i64, String> {
292292
Expression::Dict(pairs) => for (k, v) in pairs { walk_e(k, count); walk_e(v, count); }
293293
Expression::Index { index, .. } => walk_e(index, count),
294294
Expression::ChainedIndex { object, index } => { walk_e(object, count); walk_e(index, count); }
295-
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b)
295+
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b) | Expression::Power(a, b)
296296
| Expression::Eq(a, b) | Expression::Ne(a, b) | Expression::Lt(a, b) | Expression::Le(a, b) | Expression::Gt(a, b) | Expression::Ge(a, b)
297297
| Expression::And(a, b) | Expression::Or(a, b)
298298
| Expression::BitAnd(a, b) | Expression::BitOr(a, b) | Expression::BitXor(a, b)

omnimcode-core/src/compiler.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ impl Compiler {
114114
}
115115
}
116116
Expression::Mod(_, _) => Some("int"),
117+
Expression::Power(_, _) => None, // may be int or float depending on exponent sign
117118
Expression::Eq(_, _)
118119
| Expression::Ne(_, _)
119120
| Expression::Lt(_, _)
@@ -456,6 +457,9 @@ impl Compiler {
456457
self.compile_expr(r)?;
457458
self.emit(Op::Mod);
458459
}
460+
Expression::Power(_, _) => {
461+
return Err("Power (**) not supported in bytecode VM — use tree-walk interpreter".to_string());
462+
}
459463
Expression::Eq(l, r) => {
460464
let lt = self.infer_type(l);
461465
let rt = self.infer_type(r);

omnimcode-core/src/formatter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ fn format_expr(expr: &Expression, out: &mut String) {
339339
Expression::Mul(l, r) => format_binop(l, "*", r, out),
340340
Expression::Div(l, r) => format_binop(l, "/", r, out),
341341
Expression::Mod(l, r) => format_binop(l, "%", r, out),
342+
Expression::Power(l, r) => format_binop(l, "**", r, out),
342343
Expression::Eq(l, r) => format_binop(l, "==", r, out),
343344
Expression::Ne(l, r) => format_binop(l, "!=", r, out),
344345
Expression::Lt(l, r) => format_binop(l, "<", r, out),

omnimcode-core/src/interpreter.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,10 @@ impl Interpreter {
807807
Box::new(Self::rewrite_call_expr(*l, module_names, alias)),
808808
Box::new(Self::rewrite_call_expr(*r, module_names, alias)),
809809
),
810+
Expression::Power(l, r) => Expression::Power(
811+
Box::new(Self::rewrite_call_expr(*l, module_names, alias)),
812+
Box::new(Self::rewrite_call_expr(*r, module_names, alias)),
813+
),
810814
Expression::Eq(l, r) => Expression::Eq(
811815
Box::new(Self::rewrite_call_expr(*l, module_names, alias)),
812816
Box::new(Self::rewrite_call_expr(*r, module_names, alias)),
@@ -1245,6 +1249,11 @@ impl Interpreter {
12451249
}
12461250
Expression::Mod(Box::new(l), Box::new(r))
12471251
}
1252+
Expression::Power(l, r) => {
1253+
let l = Self::heal_expr(*l, defined, arities, diags);
1254+
let r = Self::heal_expr(*r, defined, arities, diags);
1255+
Expression::Power(Box::new(l), Box::new(r))
1256+
}
12481257
Expression::Call { name, args, pos } => {
12491258
// Typo check at call site. Substrate-routed lookup:
12501259
// probes the 3 hash-bucket neighborhood first, falls
@@ -2102,6 +2111,20 @@ impl Interpreter {
21022111
}
21032112
}
21042113
}
2114+
Expression::Power(base, exp) => {
2115+
let bv = self.eval_expr(base)?;
2116+
let ev = self.eval_expr(exp)?;
2117+
if bv.is_float() || ev.is_float() {
2118+
Ok(Value::HFloat(bv.to_float().powf(ev.to_float())))
2119+
} else {
2120+
let e = ev.to_int();
2121+
if e < 0 {
2122+
Ok(Value::HFloat((bv.to_int() as f64).powi(e as i32)))
2123+
} else {
2124+
Ok(Value::HInt(HInt::new(bv.to_int().pow(e as u32))))
2125+
}
2126+
}
2127+
}
21052128
Expression::Eq(l, r) => {
21062129
let lv = self.eval_expr(l)?;
21072130
let rv = self.eval_expr(r)?;

omnimcode-core/src/parser.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ pub enum Token {
7676
StarEq,
7777
SlashEq,
7878
PercentEq,
79+
StarStarEq,
80+
// Exponentiation operator
81+
StarStar,
7982
Ne,
8083
Lt,
8184
Le,
@@ -515,6 +518,14 @@ impl Lexer {
515518
}
516519
Some('*') => {
517520
self.advance();
521+
if self.current() == Some('*') {
522+
self.advance();
523+
if self.current() == Some('=') {
524+
self.advance();
525+
return Token::StarStarEq;
526+
}
527+
return Token::StarStar;
528+
}
518529
if self.current() == Some('=') {
519530
self.advance();
520531
return Token::StarEq;
@@ -1045,9 +1056,9 @@ impl Parser {
10451056
})
10461057
}
10471058
Token::PlusEq | Token::MinusEq | Token::StarEq
1048-
| Token::SlashEq | Token::PercentEq => {
1059+
| Token::SlashEq | Token::PercentEq | Token::StarStarEq => {
10491060
// Desugar `x += expr` → `x = x + expr`. Same
1050-
// for -=, *=, /=, %=. We don't introduce a new
1061+
// for -=, *=, /=, %=, **=. We don't introduce a new
10511062
// AST node — the rewrite stays inside the parser
10521063
// and the rest of the pipeline sees a normal
10531064
// Assignment with a binop on the RHS.
@@ -1062,6 +1073,7 @@ impl Parser {
10621073
Token::StarEq => Expression::Mul(Box::new(lhs), Box::new(rhs)),
10631074
Token::SlashEq => Expression::Div(Box::new(lhs), Box::new(rhs)),
10641075
Token::PercentEq => Expression::Mod(Box::new(lhs), Box::new(rhs)),
1076+
Token::StarStarEq => Expression::Power(Box::new(lhs), Box::new(rhs)),
10651077
_ => unreachable!(),
10661078
};
10671079
Ok(Statement::Assignment { name: ident, value })
@@ -1745,23 +1757,23 @@ impl Parser {
17451757
}
17461758

17471759
fn parse_multiplicative(&mut self) -> Result<Expression, String> {
1748-
let mut left = self.parse_primary()?;
1760+
let mut left = self.parse_power()?;
17491761

17501762
while matches!(self.current(), Token::Star | Token::Slash | Token::Percent) {
17511763
let expr = match self.current() {
17521764
Token::Star => {
17531765
self.advance();
1754-
let right = self.parse_primary()?;
1766+
let right = self.parse_power()?;
17551767
Expression::mul(left, right)
17561768
}
17571769
Token::Slash => {
17581770
self.advance();
1759-
let right = self.parse_primary()?;
1771+
let right = self.parse_power()?;
17601772
Expression::div(left, right)
17611773
}
17621774
Token::Percent => {
17631775
self.advance();
1764-
let right = self.parse_primary()?;
1776+
let right = self.parse_power()?;
17651777
Expression::Mod(Box::new(left), Box::new(right))
17661778
}
17671779
_ => break,
@@ -1772,6 +1784,18 @@ impl Parser {
17721784
Ok(left)
17731785
}
17741786

1787+
// Right-associative: `a ** b ** c` = `a ** (b ** c)`
1788+
fn parse_power(&mut self) -> Result<Expression, String> {
1789+
let base = self.parse_primary()?;
1790+
if self.current() == Token::StarStar {
1791+
self.advance();
1792+
let exp = self.parse_power()?; // right-assoc: recurse
1793+
Ok(Expression::Power(Box::new(base), Box::new(exp)))
1794+
} else {
1795+
Ok(base)
1796+
}
1797+
}
1798+
17751799
fn parse_primary(&mut self) -> Result<Expression, String> {
17761800
// Unary bitwise NOT: `~x`
17771801
if self.current() == Token::BitNot {

omnimcode-js/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ impl Transpiler {
624624
Expression::Mul(l, r) => format!("({} * {})", self.emit_expr(l), self.emit_expr(r)),
625625
Expression::Div(l, r) => format!("({} / {})", self.emit_expr(l), self.emit_expr(r)),
626626
Expression::Mod(l, r) => format!("({} % {})", self.emit_expr(l), self.emit_expr(r)),
627+
Expression::Power(l, r) => format!("Math.pow({}, {})", self.emit_expr(l), self.emit_expr(r)),
627628

628629
// comparisons
629630
Expression::Eq(l, r) => format!("({} === {})", self.emit_expr(l), self.emit_expr(r)),

0 commit comments

Comments
 (0)