From 310371cf8ceac379c77501a7119bcab7abafb734 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 17 Mar 2026 12:21:14 -0600 Subject: [PATCH] starlark: add ** power operator, **= augmented assignment, and pow() built-in (#631) Add the ** exponentiation operator and pow() built-in function, bringing Starlark closer to Python's numeric capabilities. Parser: introduce parseFactor/parsePower productions (modeled on Python's grammar) to place unary operators between multiplication and exponentiation in precedence, and to handle right-associativity of **. Scanner: reorder the token enum so that STARSTAR and STARSTAR_EQ are adjacent to the PLUS..GTGT and PLUS_EQ..GTGT_EQ groups, allowing the compiler and VM offset arithmetic to work without special cases. Runtime: Int**Int (non-negative exp) produces exact Int via big.Int.Exp with a 4096-bit result size cap; negative or float exponents produce Float. pow(base, exp, mod) performs modular exponentiation for integer arguments. --- doc/spec.md | 70 ++++++++++++++++++++++++++++----- internal/compile/compile.go | 8 +++- starlark/eval.go | 63 +++++++++++++++++++++++++++++ starlark/interp.go | 1 + starlark/library.go | 35 +++++++++++++++++ starlark/testdata/builtins.star | 33 ++++++++++++++++ starlark/testdata/float.star | 26 ++++++++++++ starlark/testdata/int.star | 30 ++++++++++++++ syntax/parse.go | 47 ++++++++++++++++------ syntax/parse_test.go | 24 +++++++++++ syntax/scan.go | 14 +++++-- syntax/scan_test.go | 1 + 12 files changed, 325 insertions(+), 27 deletions(-) diff --git a/doc/spec.md b/doc/spec.md index 29a38cbb..ea71927d 100644 --- a/doc/spec.md +++ b/doc/spec.md @@ -100,7 +100,7 @@ characters are tokens: += -= *= /= //= %= == != ^ < > << >> & | ^= <= >= <<= >>= &= |= -. , ; : ~ ** +. , ; : ~ ** **= ( ) [ ] { } ``` @@ -1703,11 +1703,14 @@ There are three unary operators, all appearing before their operand: `+`, `-`, `~`, and `not`. ```grammar {.good} -UnaryExpr = '+' PrimaryExpr - | '-' PrimaryExpr - | '~' PrimaryExpr +UnaryExpr = '+' UnaryExpr + | '-' UnaryExpr + | '~' UnaryExpr + | PowerExpr | 'not' Test . + +PowerExpr = PrimaryExpr ['**' UnaryExpr] . ``` ```text @@ -1717,6 +1720,10 @@ UnaryExpr = '+' PrimaryExpr not x logical negation (any type) ``` +The `+`, `-`, and `~` operators bind less tightly than `**`, so +`-x ** y` is parsed as `-(x ** y)`. However, they may appear on the +right side of `**`, so `x ** -y` is parsed as `x ** (-y)`. + The `+` and `-` operators may be applied to any number (`int` or `float`) and return the number unchanged. Unary `+` is never necessary in a correct program, @@ -1767,11 +1774,13 @@ and << >> - + * / // % +** ``` Comparison operators, `in`, and `not in` are non-associative, so the parser will not accept `0 <= i < n`. -All other binary operators of equal precedence associate to the left. +The `**` operator is right-associative; all other binary operators of +equal precedence associate to the left. ```grammar {.good} BinaryExpr = Test {Binop Test} . @@ -1788,6 +1797,10 @@ Binop = 'or' . ``` +Note: the `**` operator is not listed in `Binop` because it has +special parsing rules (right-associativity and interaction with unary +operators); see `PowerExpr` under [Unary operators](#unary-operators). + #### `or` and `and` The `or` and `and` operators yield, respectively, the logical disjunction and @@ -1888,6 +1901,7 @@ Arithmetic (int or float; result has type float unless both operands have type i number / number # real division (result is always a float) number // number # floored division number % number # remainder of floored division + number ** number # exponentiation number ^ number # bitwise XOR number << number # bitwise left shift number >> number # bitwise right shift @@ -1917,11 +1931,27 @@ Dict dict | dict # ordered union ``` -The operands of the arithmetic operators `+`, `-`, `*`, `//`, and -`%` must both be numbers (`int` or `float`) but need not have the same type. +The operands of the arithmetic operators `+`, `-`, `*`, `//`, `%`, +and `**` must both be numbers (`int` or `float`) but need not have the same type. The type of the result has type `int` only if both operands have that type. The result of real division `/` always has type `float`. +The `**` operator computes exponentiation. +It is right-associative: `2 ** 3 ** 2` is equal to `2 ** 9`, not `8 ** 2`. +When both operands are integers and the exponent is non-negative, the +result is an exact integer. +When the exponent is negative or either operand is a float, the result +is a float. +It is an error to raise a negative number to a fractional power (the +result would be complex) or to raise zero to a negative power. + +```python +2 ** 10 # 1024 +2 ** -1 # 0.5 +4.0 ** 0.5 # 2.0 +2 ** 3 ** 2 # 512 +``` + The `+` operator may be applied to non-numeric operands of the same type, such as two lists, two tuples, or two strings, in which case it computes the concatenation of the two operands and yields a new value of @@ -2510,11 +2540,11 @@ in `for` loops and in comprehensions. An augmented assignment, which has the form `lhs op= rhs` updates the variable `lhs` by applying a binary arithmetic operator `op` (one of -`+`, `-`, `*`, `/`, `//`, `%`, `&`, `|`, `^`, `<<`, `>>`) to the previous +`+`, `-`, `*`, `/`, `//`, `%`, `&`, `|`, `^`, `<<`, `>>`, `**`) to the previous value of `lhs` and the value of `rhs`. ```grammar {.good} -AssignStmt = Expression ('+=' | '-=' | '*=' | '/=' | '//=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') Expression . +AssignStmt = Expression ('+=' | '-=' | '*=' | '/=' | '//=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' | '**=') Expression . ``` The left-hand side must be a simple target: @@ -3190,6 +3220,28 @@ See also: `chr`. Implementation note: `ord` is not provided by the Java implementation. +### pow + +`pow(base, exp)` returns `base` raised to the power `exp`. +It is equivalent to the expression `base ** exp`. +The arguments must be numbers (`int` or `float`). + +With three arguments, `pow(base, exp, mod)` computes modular +exponentiation: it returns `base**exp mod mod`. +The three-argument form requires all arguments to be integers and the +modulus to be nonzero. When `exp` is negative, it computes the modular +inverse of `base` with respect to `mod`, which requires that `base` +and `mod` be coprime. + +```python +pow(2, 10) # 1024 +pow(2, -1) # 0.5 +pow(2, 10, 1000) # 24 +pow(3, -1, 7) # 5 (modular inverse: 3*5 ≡ 1 mod 7) +``` + +Implementation note: `pow` is not provided by the Java implementation. + ### print `print(*args, sep=" ")` prints its arguments, followed by a newline. diff --git a/internal/compile/compile.go b/internal/compile/compile.go index 73b506e5..093cfd4d 100644 --- a/internal/compile/compile.go +++ b/internal/compile/compile.go @@ -86,6 +86,7 @@ const ( CIRCUMFLEX LTLT GTGT + STARSTAR IN @@ -215,6 +216,7 @@ var opcodeNames = [...]string{ SLASHSLASH: "slashslash", SLICE: "slice", STAR: "star", + STARSTAR: "starstar", TILDE: "tilde", TRUE: "true", UMINUS: "uminus", @@ -289,6 +291,7 @@ var stackEffect = [...]int8{ SLASHSLASH: -1, SLICE: -3, STAR: -1, + STARSTAR: -1, TRUE: +1, UMINUS: 0, UNIVERSAL: +1, @@ -1107,7 +1110,8 @@ func (fcomp *fcomp) stmt(stmt syntax.Stmt) { syntax.PIPE_EQ, syntax.CIRCUMFLEX_EQ, syntax.LTLT_EQ, - syntax.GTGT_EQ: + syntax.GTGT_EQ, + syntax.STARSTAR_EQ: // augmented assignment: x += y var set func() @@ -1627,6 +1631,8 @@ func (fcomp *fcomp) binop(pos syntax.Position, op syntax.Token) { fcomp.emit(LTLT) case syntax.GTGT: fcomp.emit(GTGT) + case syntax.STARSTAR: + fcomp.emit(STARSTAR) case syntax.IN: fcomp.emit(IN) case syntax.NOT_IN: diff --git a/starlark/eval.go b/starlark/eval.go index 939f4beb..e52e951f 100644 --- a/starlark/eval.go +++ b/starlark/eval.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log" + "math" "math/big" "math/bits" "sort" @@ -1024,6 +1025,53 @@ func Binary(op syntax.Token, x, y Value) (Value, error) { return interpolate(string(x), y) } + case syntax.STARSTAR: + switch x := x.(type) { + case Int: + switch y := y.(type) { + case Int: + if y.Sign() < 0 { + if x.Sign() == 0 { + return nil, fmt.Errorf("zero raised to negative power") + } + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return floatPow(xf, yf) + } + // y >= 0: integer exponentiation. + if x.bigInt().BitLen() > 1 { + yInt, err := AsInt32(y) + if err != nil || int64(x.bigInt().BitLen())*int64(yInt) > 4096 { + return nil, fmt.Errorf("exponent too large") + } + } + return MakeBigInt(new(big.Int).Exp(x.bigInt(), y.bigInt(), nil)), nil + case Float: + xf, err := x.finiteFloat() + if err != nil { + return nil, err + } + return floatPow(xf, y) + } + case Float: + switch y := y.(type) { + case Float: + return floatPow(x, y) + case Int: + yf, err := y.finiteFloat() + if err != nil { + return nil, err + } + return floatPow(x, yf) + } + } + case syntax.NOT_IN: z, err := Binary(syntax.IN, x, y) if err != nil { @@ -1135,6 +1183,21 @@ unknown: return nil, fmt.Errorf("unknown binary op: %s %s %s", x.Type(), op, y.Type()) } +// floatPow computes x ** y for float operands. +func floatPow(x, y Float) (Value, error) { + if x == 0 && y < 0 { + return nil, fmt.Errorf("zero raised to negative power") + } + if x < 0 && y != Float(math.Trunc(float64(y))) { + return nil, fmt.Errorf("negative number raised to non-integer power") + } + rf := math.Pow(float64(x), float64(y)) + if math.IsInf(rf, 0) { + return nil, fmt.Errorf("floating-point result too large") + } + return Float(rf), nil +} + // It's always possible to overeat in small bites but we'll // try to stop someone swallowing the world in one gulp. const maxAlloc = 1 << 30 diff --git a/starlark/interp.go b/starlark/interp.go index a0af4cbb..ee61524b 100644 --- a/starlark/interp.go +++ b/starlark/interp.go @@ -185,6 +185,7 @@ loop: compile.CIRCUMFLEX, compile.LTLT, compile.GTGT, + compile.STARSTAR, compile.IN: binop := syntax.Token(op-compile.PLUS) + syntax.PLUS if op == compile.IN { diff --git a/starlark/library.go b/starlark/library.go index 9e6bd2c9..4b8d6ddc 100644 --- a/starlark/library.go +++ b/starlark/library.go @@ -59,6 +59,7 @@ func init() { "max": NewBuiltin("max", minmax), "min": NewBuiltin("min", minmax), "ord": NewBuiltin("ord", ord), + "pow": NewBuiltin("pow", pow), "print": NewBuiltin("print", print), "range": NewBuiltin("range", range_), "repr": NewBuiltin("repr", repr), @@ -795,6 +796,40 @@ func ord(thread *Thread, _ *Builtin, args Tuple, kwargs []Tuple) (Value, error) } } +// pow(base, exp[, mod]) — exponentiation. +// +// With two arguments, pow(x, y) is equivalent to x ** y. +// With three arguments, pow(x, y, z) computes x**y mod z. +// The three-argument form requires all arguments to be integers. +func pow(thread *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) { + var x, y, z Value + if err := UnpackPositionalArgs("pow", args, kwargs, 2, &x, &y, &z); err != nil { + return nil, err + } + + // Three-argument form: all arguments must be integers. + if z != nil { + xi, xok := x.(Int) + yi, yok := y.(Int) + zi, zok := z.(Int) + if !xok || !yok || !zok { + return nil, fmt.Errorf("pow: 3rd argument not allowed unless all arguments are integers") + } + if zi.Sign() == 0 { + return nil, fmt.Errorf("pow: integer modulo by zero") + } + // big.Int.Exp returns nil if y < 0 and x and m are not coprime. + result := new(big.Int).Exp(xi.bigInt(), yi.bigInt(), zi.bigInt()) + if result == nil { + return nil, fmt.Errorf("pow: base is not invertible for the given modulus") + } + return MakeBigInt(result), nil + } + + // Two-argument form: equivalent to x ** y. + return Binary(syntax.STARSTAR, x, y) +} + // https://github.com/google/starlark-go/blob/master/doc/spec.md#print func print(thread *Thread, b *Builtin, args Tuple, kwargs []Tuple) (Value, error) { sep := " " diff --git a/starlark/testdata/builtins.star b/starlark/testdata/builtins.star index a3d1ede8..c7e13cb5 100644 --- a/starlark/testdata/builtins.star +++ b/starlark/testdata/builtins.star @@ -88,6 +88,39 @@ assert.eq(dict([(1, 2), (3, 4)], foo="bar"), {1: 2, 3: 4, "foo": "bar"}) assert.eq(dict({1:2, 3:4}), {1: 2, 3: 4}) assert.eq(dict({1:2, 3:4}.items()), {1: 2, 3: 4}) +# pow +# two-argument form (equivalent to **) +assert.eq(pow(2, 0), 1) +assert.eq(pow(2, 10), 1024) +assert.eq(pow(0, 0), 1) +assert.eq(pow(-2, 3), -8) +assert.eq(pow(-2, 2), 4) +assert.eq(pow(2, -1), 0.5) +assert.eq(pow(2.0, 3), 8.0) +assert.eq(pow(4.0, 0.5), 2.0) +assert.eq(pow(4, 0.5), 2.0) +assert.eq(type(pow(2, 3)), "int") +assert.eq(type(pow(2, -1)), "float") +assert.eq(type(pow(2.0, 3)), "float") +# three-argument form (modular exponentiation) +assert.eq(pow(2, 10, 1000), 24) +assert.eq(pow(2, 10, 3), 1) +assert.eq(pow(2, 100, 13), 3) +assert.eq(pow(3, 0, 5), 1) +assert.eq(pow(100, 2, 7), 4) +assert.eq(pow(-2, 3, 5), 2) # (-8) mod 5 = 2 +# three-argument form with negative exponent (modular inverse) +assert.eq(pow(3, -1, 7), 5) # 3*5 = 15 ≡ 1 (mod 7) +assert.eq(pow(2, -1, 7), 4) # 2*4 = 8 ≡ 1 (mod 7) +# errors +assert.fails(lambda: pow(0, -1), "zero raised to negative power") +assert.fails(lambda: pow(2, 10000), "exponent too large") +assert.fails(lambda: pow(1.0, 2, 3), "3rd argument not allowed unless all arguments are integers") +assert.fails(lambda: pow(1, 2.0, 3), "3rd argument not allowed unless all arguments are integers") +assert.fails(lambda: pow(1, 2, 0), "integer modulo by zero") +assert.fails(lambda: pow(2, -1, 6), "base is not invertible for the given modulus") +assert.fails(lambda: pow("a", 2), "unknown binary op: string \\*\\* int") + # range assert.eq("range", type(range(10))) assert.eq("range(10)", str(range(0, 10, 1))) diff --git a/starlark/testdata/float.star b/starlark/testdata/float.star index 9927dfa8..e8ea1a93 100644 --- a/starlark/testdata/float.star +++ b/starlark/testdata/float.star @@ -219,6 +219,32 @@ assert.fails(lambda: 1.0 % 0, "floating-point modulo by zero") assert.fails(lambda: 1.0 % 0.0, "floating-point modulo by zero") assert.fails(lambda: 1 % 0.0, "floating-point modulo by zero") +# exponentiation +assert.eq(2.0 ** 3.0, 8.0) +assert.eq(4.0 ** 0.5, 2.0) +assert.eq(9.0 ** 0.5, 3.0) +assert.eq(2.0 ** -1.0, 0.5) +assert.eq(0.0 ** 0, 1.0) +assert.eq(0.0 ** 0.0, 1.0) +assert.eq(type(2.0 ** 3), "float") +assert.eq(type(2 ** 3.0), "float") +assert.eq(type(2.0 ** 3.0), "float") +# mixed int/float +assert.eq(2 ** 0.5, 2.0 ** 0.5) +assert.eq(2.0 ** 3, 8.0) +assert.eq(4 ** 0.5, 2.0) +# negative base with integer exponent +assert.eq((-2.0) ** 3, -8.0) +assert.eq((-2.0) ** 2, 4.0) +# errors +assert.fails(lambda: 0.0 ** -1, "zero raised to negative power") +assert.fails(lambda: 0.0 ** -1.0, "zero raised to negative power") +assert.fails(lambda: 0 ** -1.0, "zero raised to negative power") +assert.fails(lambda: (-1.0) ** 0.5, "negative number raised to non-integer power") +assert.fails(lambda: (-2) ** 0.5, "negative number raised to non-integer power") +assert.fails(lambda: 1e308 ** 2, "floating-point result too large") +assert.fails(lambda: 1e308 ** 2.0, "floating-point result too large") + # floats cannot be used as indices, even if integral assert.fails(lambda: "abc"[1.0], "want int") assert.fails(lambda: ["A", "B", "C"].insert(1.0, "D"), "want int") diff --git a/starlark/testdata/int.star b/starlark/testdata/int.star index f0e2cde3..448621a8 100644 --- a/starlark/testdata/int.star +++ b/starlark/testdata/int.star @@ -212,6 +212,36 @@ assert.eq(2 >> 1, 1) assert.fails(lambda: 2 << -1, "negative shift count") assert.fails(lambda: 1 << 512, "shift count too large") +# exponentiation +assert.eq(2 ** 0, 1) +assert.eq(2 ** 1, 2) +assert.eq(2 ** 10, 1024) +assert.eq(0 ** 0, 1) +assert.eq(1 ** 1000, 1) +assert.eq((-1) ** 0, 1) +assert.eq((-1) ** 1, -1) +assert.eq((-1) ** 2, 1) +assert.eq((-2) ** 2, 4) +assert.eq((-2) ** 3, -8) +assert.eq(3 ** 3, 27) +assert.eq(10 ** 100, 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) +assert.eq(type(2 ** 10), "int") +# right-associativity +assert.eq(2 ** 3 ** 2, 512) # 2 ** (3 ** 2) = 2 ** 9 = 512 +# augmented assignment +def augmented_pow(): + x = 2 + x **= 10 + assert.eq(x, 1024) +augmented_pow() +# negative exponent: result is float +assert.eq(2 ** -1, 0.5) +assert.eq(4 ** -1, 0.25) +assert.eq(type(2 ** -1), "float") +# errors +assert.fails(lambda: 0 ** -1, "zero raised to negative power") +assert.fails(lambda: 2 ** 10000, "exponent too large") + # comparisons # TODO(adonovan): test: < > == != etc def comparisons(): diff --git a/syntax/parse.go b/syntax/parse.go index 9430fb30..0fd16a4d 100644 --- a/syntax/parse.go +++ b/syntax/parse.go @@ -329,7 +329,7 @@ func (p *parser) parseSmallStmt() Stmt { // Assignment x := p.parseExpr(false) switch p.tok { - case EQ, PLUS_EQ, MINUS_EQ, STAR_EQ, SLASH_EQ, SLASHSLASH_EQ, PERCENT_EQ, AMP_EQ, PIPE_EQ, CIRCUMFLEX_EQ, LTLT_EQ, GTGT_EQ: + case EQ, PLUS_EQ, MINUS_EQ, STAR_EQ, SLASH_EQ, SLASHSLASH_EQ, PERCENT_EQ, AMP_EQ, PIPE_EQ, CIRCUMFLEX_EQ, LTLT_EQ, GTGT_EQ, STARSTAR_EQ: op := p.tok pos := p.nextToken() // consume op rhs := p.parseExpr(false) @@ -594,7 +594,7 @@ func (p *parser) parseLambda(allowCond bool) Expr { func (p *parser) parseTestPrec(prec int) Expr { if prec >= len(preclevels) { - return p.parsePrimaryWithSuffix() + return p.parseFactor() } // expr = NOT expr @@ -650,7 +650,8 @@ var precedence [maxToken]int8 // preclevels groups operators of equal precedence. // Comparisons are nonassociative; other binary operators associate to the left. -// Unary MINUS, unary PLUS, and TILDE have higher precedence so are handled in parsePrimary. +// Unary MINUS, PLUS, and TILDE are handled in parseFactor. +// Exponentiation (**) is right-associative and handled in parsePower. // See https://github.com/google/starlark-go/blob/master/doc/spec.md#binary-operators var preclevels = [...][]Token{ {OR}, // or @@ -677,6 +678,36 @@ func init() { } } +// parseFactor parses a unary expression or power expression. +// +// factor = ('-'|'+'|'~') factor | power +func (p *parser) parseFactor() Expr { + if p.tok == MINUS || p.tok == PLUS || p.tok == TILDE { + tok := p.tok + pos := p.nextToken() + x := p.parseFactor() + return &UnaryExpr{ + OpPos: pos, + Op: tok, + X: x, + } + } + return p.parsePower() +} + +// parsePower parses a power expression (right-associative **). +// +// power = primary_with_suffix ('**' factor)? +func (p *parser) parsePower() Expr { + x := p.parsePrimaryWithSuffix() + if p.tok == STARSTAR { + pos := p.nextToken() + y := p.parseFactor() + return &BinaryExpr{OpPos: pos, Op: STARSTAR, X: x, Y: y} + } + return x +} + // primary_with_suffix = primary // // | primary '.' IDENT @@ -804,7 +835,6 @@ func (p *parser) parseArgs() []Expr { // | '[' ... // list literal or comprehension // | '{' ... // dict literal or comprehension // | '(' ... // tuple or parenthesized expression -// | ('-'|'+'|'~') primary_with_suffix func (p *parser) parsePrimary() Expr { switch p.tok { case IDENT: @@ -850,15 +880,6 @@ func (p *parser) parsePrimary() Expr { Rparen: rparen, } - case MINUS, PLUS, TILDE: // unary - tok := p.tok - pos := p.nextToken() - x := p.parsePrimaryWithSuffix() - return &UnaryExpr{ - OpPos: pos, - Op: tok, - X: x, - } } // Report start pos of final token as it may be a NEWLINE (#532). diff --git a/syntax/parse_test.go b/syntax/parse_test.go index 197e9050..1efed1b8 100644 --- a/syntax/parse_test.go +++ b/syntax/parse_test.go @@ -98,6 +98,28 @@ func TestExprParseTrees(t *testing.T) { `(BinaryExpr X=(UnaryExpr Op=- X=1) Op=* Y=2)`}, {`-x[i]`, // prec(unary -) < prec(x[i]) `(UnaryExpr Op=- X=(IndexExpr X=x Y=i))`}, + {`2 ** 3`, // power + `(BinaryExpr X=2 Op=** Y=3)`}, + {`2 ** 3 ** 4`, // power is right-associative + `(BinaryExpr X=2 Op=** Y=(BinaryExpr X=3 Op=** Y=4))`}, + {`-2 ** 2`, // ** binds tighter than unary - on its left + `(UnaryExpr Op=- X=(BinaryExpr X=2 Op=** Y=2))`}, + {`2 ** -2`, // unary - allowed on right of ** + `(BinaryExpr X=2 Op=** Y=(UnaryExpr Op=- X=2))`}, + {`~2 ** 2`, // ** binds tighter than unary ~ on its left + `(UnaryExpr Op=~ X=(BinaryExpr X=2 Op=** Y=2))`}, + {`x ** y ** z`, // right-associative with identifiers + `(BinaryExpr X=x Op=** Y=(BinaryExpr X=y Op=** Y=z))`}, + {`a * b ** c`, // ** binds tighter than * + `(BinaryExpr X=a Op=* Y=(BinaryExpr X=b Op=** Y=c))`}, + {`a ** b * c`, // ** binds tighter than * + `(BinaryExpr X=(BinaryExpr X=a Op=** Y=b) Op=* Y=c)`}, + {`x[i] ** 2`, // primary suffix on left of ** + `(BinaryExpr X=(IndexExpr X=x Y=i) Op=** Y=2)`}, + {`x.f ** 2`, // dot suffix on left of ** + `(BinaryExpr X=(DotExpr X=x Name=f) Op=** Y=2)`}, + {`f() ** 2`, // call suffix on left of ** + `(BinaryExpr X=(CallExpr Fn=f) Op=** Y=2)`}, {`a | b & c | d`, // prec(|) < prec(&) `(BinaryExpr X=(BinaryExpr X=a Op=| Y=(BinaryExpr X=b Op=& Y=c)) Op=| Y=d)`}, {`a or b and c or d`, @@ -179,6 +201,8 @@ else: `(DefStmt Name=f Params=((UnaryExpr Op=** X=kwargs) (UnaryExpr Op=* X=args)) Body=((BranchStmt Token=pass)))`}, {`def f(a, b, c=d): pass`, `(DefStmt Name=f Params=(a b (BinaryExpr X=c Op== Y=d)) Body=((BranchStmt Token=pass)))`}, + {`x **= 2`, + `(AssignStmt Op=**= LHS=x RHS=2)`}, {`def f(a, b=c, d): pass`, `(DefStmt Name=f Params=(a (BinaryExpr X=b Op== Y=c) d) Body=((BranchStmt Token=pass)))`}, // TODO(adonovan): fix this {`def f(): diff --git a/syntax/scan.go b/syntax/scan.go index 894cf7f7..e56d2ece 100644 --- a/syntax/scan.go +++ b/syntax/scan.go @@ -48,6 +48,7 @@ const ( CIRCUMFLEX // ^ LTLT // << GTGT // >> + STARSTAR // ** TILDE // ~ DOT // . COMMA // , @@ -66,7 +67,7 @@ const ( LE // <= EQL // == NEQ // != - PLUS_EQ // += (keep order consistent with PLUS..GTGT) + PLUS_EQ // += (keep order consistent with PLUS..STARSTAR) MINUS_EQ // -= STAR_EQ // *= SLASH_EQ // /= @@ -77,7 +78,7 @@ const ( CIRCUMFLEX_EQ // ^= LTLT_EQ // <<= GTGT_EQ // >>= - STARSTAR // ** + STARSTAR_EQ // **= // Keywords AND @@ -126,7 +127,7 @@ func (tok Token) String() string { return tokenNames[tok] } // GoString is like String but quotes punctuation tokens. // Use Sprintf("%#v", tok) when constructing error messages. func (tok Token) GoString() string { - if tok >= PLUS && tok <= STARSTAR { + if tok >= PLUS && tok <= STARSTAR_EQ { return "'" + tokenNames[tok] + "'" } return tokenNames[tok] @@ -183,6 +184,7 @@ var tokenNames = [...]string{ LTLT_EQ: "<<=", GTGT_EQ: ">>=", STARSTAR: "**", + STARSTAR_EQ: "**=", AND: "and", BREAK: "break", CONTINUE: "continue", @@ -846,11 +848,15 @@ start: } panic("unreachable") - case '*': // possibly followed by '*' or '=' + case '*': // possibly followed by '*', '**=', or '=' sc.readRune() switch sc.peekRune() { case '*': sc.readRune() + if sc.peekRune() == '=' { + sc.readRune() + return STARSTAR_EQ + } return STARSTAR case '=': sc.readRune() diff --git a/syntax/scan_test.go b/syntax/scan_test.go index e9465926..b6c6c0d6 100644 --- a/syntax/scan_test.go +++ b/syntax/scan_test.go @@ -68,6 +68,7 @@ func TestScanner(t *testing.T) { {`print(x); print(y)`, "print ( x ) ; print ( y ) EOF"}, {"\nprint(\n1\n)\n", "print ( 1 ) newline EOF"}, // final \n is at toplevel on non-blank line => token {`/ // /= //= ///=`, "/ // /= //= // /= EOF"}, + {`** **= ***=`, "** **= ** *= EOF"}, {`# hello print(x)`, "print ( x ) EOF"}, {`# hello