starlark: add support for addressing
This change adds support for addressable variables and the unary
prefix operators * and &, which are needed to correctly support
Stargo (see wip-stargo branch), an upcoming extension that enables
Starlark bindings for Go types and variables.
Start reading at the starlark.Variable interface for background.
All new features are behind the -addressing flag.
Change-Id: I0258f5d38f85c0d03bb08dc98165b0335a88a98a
diff --git a/doc/spec.md b/doc/spec.md
index d27cf52..8c0df31 100644
--- a/doc/spec.md
+++ b/doc/spec.md
@@ -1505,7 +1505,7 @@
| ListExpr | ListComp
| DictExpr | DictComp
| '(' [Expression] [,] ')'
- | ('-' | '+') PrimaryExpr
+ | ('-' | '+' | '*' | '&') PrimaryExpr
.
DotSuffix = '.' identifier .
@@ -1637,13 +1637,15 @@
### Unary operators
-There are three unary operators, all appearing before their operand:
-`+`, `-`, `~`, and `not`.
+Starlark has the following unary operators, which all appear before their operand:
+`+`, `-`, `~`, `*`, `&`, and `not`.
```grammar {.good}
UnaryExpr = '+' PrimaryExpr
| '-' PrimaryExpr
| '~' PrimaryExpr
+ | '*' PrimaryExpr
+ | '&' PrimaryExpr
| 'not' Test
.
```
@@ -1690,9 +1692,21 @@
~0 # -1
```
+The Go implementation of Starlark additionally has the unary
+prefix operators `*` and `&`, in order to support Stargo, which
+provides Starlark bindings for Go variables.
+These operators are not part of standard Starlark, and must be enabled
+in Go by the `-addressing` flag.
+
+The expression `*ptr` dereferences a pointer value, `ptr`.
+The expression `&expr` returns the address of an expression `expr`;
+it may be applied only to field selection or index expressions
+such as `&a[i]`, `&x.f`, or `&x[i].f[j].g`.
+Consult the Stargo documentation for more details.
+
<b>Implementation note:</b>
The parser in the Java implementation of Starlark does not accept unary
-`+` and `~` expressions.
+`+` and `~` expressions, nor `*` and `&` as unary operators.
### Binary operators
diff --git a/internal/compile/compile.go b/internal/compile/compile.go
index 67f3ae8..288da44 100644
--- a/internal/compile/compile.go
+++ b/internal/compile/compile.go
@@ -37,7 +37,7 @@
const debug = false // TODO(adonovan): use a bitmap of options; and regexp to match files
// Increment this to force recompilation of saved bytecode files.
-const Version = 5
+const Version = 6
type Opcode uint8
@@ -80,9 +80,10 @@
IN
- // unary operators
+ // unary operators (order of +/-/* must match Token)
UPLUS // x UPLUS x
UMINUS // x UMINUS -x
+ USTAR // x USTAR *x
TILDE // x TILDE ~x
NONE // - NONE None
@@ -94,13 +95,16 @@
NOT // value NOT bool
RETURN // value RETURN -
SETINDEX // a i new SETINDEX -
- INDEX // a i INDEX elem
+ INDEX // a i INDEX elem elem = a[i]
SETDICT // dict key value SETDICT -
SETDICTUNIQ // dict key value SETDICTUNIQ -
APPEND // list elem APPEND -
SLICE // x lo hi step SLICE slice
- INPLACE_ADD // x y INPLACE_ADD z where z is x+y or x.extend(y)
+ INPLACE_ADD // x y INPLACE_ADD z where z is x+y or x.extend(y)
MAKEDICT // - MAKEDICT dict
+ ADDRESS // var ADDRESS addr fails if not Variable [stargo only]
+ VALUE // var VALUE value identity if not Variable [stargo only]
+ SETVALUE // var x SETVALUE - fails if not Variable [stargo only]
// --- opcodes with an argument must go below this line ---
@@ -139,6 +143,7 @@
// TODO(adonovan): add dynamic checks for missing opcodes in the tables below.
var opcodeNames = [...]string{
+ ADDRESS: "address",
AMP: "amp",
APPEND: "append",
ATTR: "attr",
@@ -186,12 +191,14 @@
POP: "pop",
PREDECLARED: "predeclared",
RETURN: "return",
+ VALUE: "value",
SETDICT: "setdict",
SETDICTUNIQ: "setdictuniq",
SETFIELD: "setfield",
SETGLOBAL: "setglobal",
SETINDEX: "setindex",
SETLOCAL: "setlocal",
+ SETVALUE: "setvalue",
SLASH: "slash",
SLASHSLASH: "slashslash",
SLICE: "slice",
@@ -202,6 +209,7 @@
UNIVERSAL: "universal",
UNPACK: "unpack",
UPLUS: "uplus",
+ USTAR: "ustar",
}
const variableStackEffect = 0x7f
@@ -209,6 +217,7 @@
// stackEffect records the effect on the size of the operand stack of
// each kind of instruction. For some instructions this requires computation.
var stackEffect = [...]int8{
+ ADDRESS: 0,
AMP: -1,
APPEND: -2,
ATTR: 0,
@@ -261,6 +270,7 @@
SETGLOBAL: -1,
SETINDEX: -3,
SETLOCAL: -1,
+ SETVALUE: -2,
SLASH: -1,
SLASHSLASH: -1,
SLICE: -3,
@@ -270,6 +280,8 @@
UNIVERSAL: +1,
UNPACK: variableStackEffect,
UPLUS: 0,
+ USTAR: 0,
+ VALUE: 0,
}
func (op Opcode) String() string {
@@ -1005,13 +1017,26 @@
fcomp.set(lhs)
}
+ case *syntax.UnaryExpr:
+ // *x = ...
+ fcomp.expr(lhs.X)
+ fcomp.emit(DUP)
+ fcomp.emit(VALUE)
+ set = func() {
+ fcomp.setPos(lhs.OpPos) // op == STAR
+ fcomp.emit(SETVALUE)
+ }
+
case *syntax.IndexExpr:
// x[y] = ...
- fcomp.expr(lhs.X)
+ fcomp.addr(lhs.X)
fcomp.expr(lhs.Y)
fcomp.emit(DUP2)
fcomp.setPos(lhs.Lbrack)
fcomp.emit(INDEX)
+ if resolve.AllowAddressing {
+ fcomp.emit(VALUE)
+ }
set = func() {
fcomp.setPos(lhs.Lbrack)
fcomp.emit(SETINDEX)
@@ -1019,11 +1044,14 @@
case *syntax.DotExpr:
// x.f = ...
- fcomp.expr(lhs.X)
+ fcomp.addr(lhs.X)
fcomp.emit(DUP)
name := fcomp.pcomp.nameIndex(lhs.Name.Name)
fcomp.setPos(lhs.Dot)
fcomp.emit1(ATTR, name)
+ if resolve.AllowAddressing {
+ fcomp.emit(VALUE)
+ }
set = func() {
fcomp.setPos(lhs.Dot)
fcomp.emit1(SETFIELD, name)
@@ -1143,7 +1171,7 @@
case *syntax.IndexExpr:
// x[y] = rhs
- fcomp.expr(lhs.X)
+ fcomp.addr(lhs.X)
fcomp.emit(EXCH)
fcomp.expr(lhs.Y)
fcomp.emit(EXCH)
@@ -1152,11 +1180,19 @@
case *syntax.DotExpr:
// x.f = rhs
- fcomp.expr(lhs.X)
+ fcomp.addr(lhs.X)
fcomp.emit(EXCH)
fcomp.setPos(lhs.Dot)
fcomp.emit1(SETFIELD, fcomp.pcomp.nameIndex(lhs.Name.Name))
+ case *syntax.UnaryExpr:
+ // *x = rhs
+ fcomp.expr(lhs.X)
+ fcomp.emit(USTAR)
+ fcomp.emit(EXCH)
+ fcomp.setPos(lhs.OpPos) // op == STAR
+ fcomp.emit(SETVALUE)
+
default:
panic(lhs)
}
@@ -1170,6 +1206,47 @@
}
}
+// addr emits code for a chain of x.f and a[i] operations such as a.f[i].g[j].
+//
+// In Addressing mode, all nonrecursive calls to addr() (a chain of ATTR
+// and INDEX ops) must be followed by one of ADDRESS, SETFIELD,
+// SETINDEX, or VALUE:
+//
+// &(expr.f[i].g) expr ATTR<f> i INDEX ATTR<g> ADDRESS
+// (expr.f[i].g).x = y expr ATTR<f> i INDEX ATTR<g> y SETFIELD<x>
+// (expr.f[i].g)[j] = y expr ATTR<f> i INDEX ATTR<g> y j SETINDEX
+// _ = expr.f[i].g expr ATTR<f> i INDEX ATTR<g> VALUE
+//
+// We could in principle extend this scheme to all variables (such as x
+// in x.f)), not just the x[i] and x.f operations applied to them, but
+// it would add a cost to every variable lookup, and in practice it is
+// unnecessary as most starlark.Variables live in a module such as http
+// for http.DefaultServeMux, so they are acccessed using a dot expression.
+//
+// See comments at starlark.Variable for background.
+//
+func (fcomp *fcomp) addr(e syntax.Expr) {
+ switch e := e.(type) {
+ case *syntax.ParenExpr:
+ fcomp.addr(e.X)
+
+ case *syntax.DotExpr:
+ fcomp.addr(e.X)
+ fcomp.setPos(e.Dot)
+ fcomp.emit1(ATTR, fcomp.pcomp.nameIndex(e.Name.Name))
+
+ case *syntax.IndexExpr:
+ fcomp.addr(e.X)
+ fcomp.expr(e.Y)
+ fcomp.setPos(e.Lbrack)
+ fcomp.emit(INDEX)
+
+ default:
+ fcomp.expr(e)
+
+ }
+}
+
func (fcomp *fcomp) expr(e syntax.Expr) {
switch e := e.(type) {
case *syntax.ParenExpr:
@@ -1207,10 +1284,10 @@
fcomp.block = done
case *syntax.IndexExpr:
- fcomp.expr(e.X)
- fcomp.expr(e.Y)
- fcomp.setPos(e.Lbrack)
- fcomp.emit(INDEX)
+ fcomp.addr(e)
+ if resolve.AllowAddressing {
+ fcomp.emit(VALUE)
+ }
case *syntax.SliceExpr:
fcomp.setPos(e.Lbrack)
@@ -1255,13 +1332,23 @@
}
case *syntax.UnaryExpr:
- fcomp.expr(e.X)
+ switch e.Op {
+ case syntax.AMP, syntax.STAR:
+ fcomp.addr(e.X)
+ default:
+ fcomp.expr(e.X)
+ }
fcomp.setPos(e.OpPos)
switch e.Op {
+ case syntax.AMP:
+ fcomp.emit(ADDRESS)
case syntax.MINUS:
fcomp.emit(UMINUS)
case syntax.PLUS:
fcomp.emit(UPLUS)
+ case syntax.STAR:
+ fcomp.emit(USTAR)
+ fcomp.emit(VALUE)
case syntax.NOT:
fcomp.emit(NOT)
case syntax.TILDE:
@@ -1317,9 +1404,10 @@
}
case *syntax.DotExpr:
- fcomp.expr(e.X)
- fcomp.setPos(e.Dot)
- fcomp.emit1(ATTR, fcomp.pcomp.nameIndex(e.Name.Name))
+ fcomp.addr(e)
+ if resolve.AllowAddressing {
+ fcomp.emit(VALUE)
+ }
case *syntax.CallExpr:
fcomp.call(e)
diff --git a/resolve/resolve.go b/resolve/resolve.go
index 9c5a0b8..0393779 100644
--- a/resolve/resolve.go
+++ b/resolve/resolve.go
@@ -96,6 +96,7 @@
AllowGlobalReassign = false // allow reassignment to globals declared in same file (deprecated)
AllowBitwise = false // allow bitwise operations (&, |, ^, ~, <<, and >>)
AllowRecursion = false // allow while statements and recursive functions
+ AllowAddressing = false // allow &a.f and l-value addressing of a.f[i].g
)
// File resolves the specified file.
@@ -521,15 +522,18 @@
// x = ...
allowRebind := isAugmented
r.bind(lhs, allowRebind)
+ return
case *syntax.IndexExpr:
// x[i] = ...
r.expr(lhs.X)
r.expr(lhs.Y)
+ return
case *syntax.DotExpr:
// x.f = ...
r.expr(lhs.X)
+ return
case *syntax.TupleExpr:
// (x, y) = ...
@@ -542,6 +546,7 @@
for _, elem := range lhs.List {
r.assign(elem, isAugmented)
}
+ return
case *syntax.ListExpr:
// [x, y, z] = ...
@@ -554,14 +559,24 @@
for _, elem := range lhs.List {
r.assign(elem, isAugmented)
}
+ return
case *syntax.ParenExpr:
r.assign(lhs.X, isAugmented)
+ return
- default:
- name := strings.ToLower(strings.TrimPrefix(fmt.Sprintf("%T", lhs), "*syntax."))
- r.errorf(syntax.Start(lhs), "can't assign to %s", name)
+ case *syntax.UnaryExpr:
+ if lhs.Op == syntax.STAR {
+ if !AllowAddressing {
+ r.errorf(lhs.OpPos, doesnt+"support address operations")
+ }
+ r.expr(lhs.X)
+ return
+ }
}
+
+ name := strings.ToLower(strings.TrimPrefix(fmt.Sprintf("%T", lhs), "*syntax."))
+ r.errorf(syntax.Start(lhs), "can't assign to %s", name)
}
func (r *resolver) expr(e syntax.Expr) {
@@ -647,6 +662,23 @@
if !AllowBitwise && e.Op == syntax.TILDE {
r.errorf(e.OpPos, doesnt+"support bitwise operations")
}
+ if e.Op == syntax.STAR {
+ if !AllowAddressing {
+ r.errorf(e.OpPos, doesnt+"support address operations")
+ }
+ }
+ if e.Op == syntax.AMP {
+ if !AllowAddressing {
+ r.errorf(e.OpPos, doesnt+"support address operations")
+ } else {
+ switch unparen(e.X).(type) {
+ case *syntax.DotExpr, *syntax.IndexExpr:
+ // ok
+ default:
+ r.errorf(e.OpPos, "& operator can be applied only to &x.f or &a[i]")
+ }
+ }
+ }
r.expr(e.X)
case *syntax.BinaryExpr:
@@ -688,7 +720,7 @@
r.errorf(pos, "multiple *args not allowed")
}
seenVarargs = true
- r.expr(arg)
+ r.expr(unop.X)
} else if binop, ok := arg.(*syntax.BinaryExpr); ok && binop.Op == syntax.EQ {
// k=v
n++
@@ -891,3 +923,13 @@
}
return bind
}
+
+func unparen(e syntax.Expr) syntax.Expr {
+ for {
+ paren, ok := e.(*syntax.ParenExpr)
+ if !ok {
+ return e
+ }
+ e = paren.X
+ }
+}
diff --git a/resolve/resolve_test.go b/resolve/resolve_test.go
index 2885a5b..caa80fa 100644
--- a/resolve/resolve_test.go
+++ b/resolve/resolve_test.go
@@ -15,6 +15,7 @@
)
func setOptions(src string) {
+ resolve.AllowAddressing = option(src, "addressing")
resolve.AllowBitwise = option(src, "bitwise")
resolve.AllowFloat = option(src, "float")
resolve.AllowGlobalReassign = option(src, "globalreassign")
diff --git a/resolve/testdata/resolve.star b/resolve/testdata/resolve.star
index bd038ce..4eaefb5 100644
--- a/resolve/testdata/resolve.star
+++ b/resolve/testdata/resolve.star
@@ -311,3 +311,47 @@
# https://github.com/bazelbuild/starlark/starlark/issues/21
def f(**kwargs): pass
f(a=1, a=1) ### `keyword argument a repeated`
+
+---
+ptr = &U.x ### "dialect does not support address operations"
+
+x = *ptr ### "dialect does not support address operations"
+
+---
+# The & operator can be applied only to &x.f or &a[i].
+# option:addressing
+
+x, i, j = U
+
+def use(x): pass
+
+# ok
+use(&x.f)
+use(&x[i])
+use(&x.f[i].g[j])
+use(&(x.f[i].g[j]))
+use(&x[i])
+(&x.f).f = 1
+
+# bad
+use(&x) ### `& operator can be applied only to &x.f or &a\[i\]`
+use(&(1+2)) ### `& operator can be applied only to &x.f or &a\[i\]`
+use(&x(x)) ### `& operator can be applied only to &x.f or &a\[i\]`
+
+
+---
+# The * operator dereferences a pointer, or creates a pointer type.
+# option:addressing
+
+ptr, f, int = U
+
+intptr = *int
+
+*ptr = 1
+y = *ptr
+
++ptr = 1 ### `can't assign to unaryexpr`
+
+f(*ptr) # ok, but means varargs, not dereference
+f(*ptr, 0) ### `argument may not follow \*args`
+f((*ptr), 0) # the workaround
diff --git a/starlark/eval_test.go b/starlark/eval_test.go
index 6c5c0dc..60b8be9 100644
--- a/starlark/eval_test.go
+++ b/starlark/eval_test.go
@@ -15,12 +15,14 @@
"go.starlark.net/internal/chunkedfile"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
+ "go.starlark.net/starlarkstruct"
"go.starlark.net/starlarktest"
"go.starlark.net/syntax"
)
// A test may enable non-standard options by containing (e.g.) "option:recursion".
func setOptions(src string) {
+ resolve.AllowAddressing = option(src, "addressing")
resolve.AllowBitwise = option(src, "bitwise")
resolve.AllowFloat = option(src, "float")
resolve.AllowGlobalReassign = option(src, "globalreassign")
@@ -99,11 +101,14 @@
}
func TestExecFile(t *testing.T) {
+ var v starlark.Value = starlark.None
+
defer setOptions("")
testdata := starlarktest.DataFile("starlark", ".")
thread := &starlark.Thread{Load: load}
starlarktest.SetReporter(thread, t)
for _, file := range []string{
+ "testdata/address.star",
"testdata/assign.star",
"testdata/bool.star",
"testdata/builtins.star",
@@ -121,9 +126,13 @@
} {
filename := filepath.Join(testdata, file)
for _, chunk := range chunkedfile.Read(filename, t) {
+
predeclared := starlark.StringDict{
"hasfields": starlark.NewBuiltin("hasfields", newHasFields),
"fibonacci": fib{},
+ "addr": starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
+ "v": variable{&v},
+ }),
}
setOptions(chunk.Source)
@@ -583,3 +592,36 @@
}
}
}
+
+type variable struct{ ptr *starlark.Value } // non-nil
+
+var _ starlark.Variable = variable{}
+
+func (v variable) Address() starlark.Value { return pointer{v.ptr} }
+func (v variable) Value() starlark.Value { return *v.ptr }
+func (v variable) SetValue(x starlark.Value) error { *v.ptr = x; return nil }
+
+func (v variable) String() string { return fmt.Sprintf("variable(%s)", *v.ptr) }
+func (v variable) Type() string { return "variable" }
+func (v variable) Truth() starlark.Bool { return true }
+func (v variable) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: variable") }
+func (v variable) Freeze() {}
+
+type pointer struct{ ptr *starlark.Value }
+
+var _ starlark.HasUnary = pointer{}
+
+func (p pointer) String() string { return "pointer" }
+func (p pointer) Type() string { return "pointer" }
+func (p pointer) Truth() starlark.Bool { return true }
+func (p pointer) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: pointer") }
+func (p pointer) Freeze() {}
+func (p pointer) Unary(op syntax.Token) (starlark.Value, error) {
+ if op == syntax.STAR {
+ if p.ptr == nil {
+ return nil, fmt.Errorf("nil dereference")
+ }
+ return variable{p.ptr}, nil
+ }
+ return nil, nil
+}
diff --git a/starlark/interp.go b/starlark/interp.go
index fe332e8..2a67890 100644
--- a/starlark/interp.go
+++ b/starlark/interp.go
@@ -154,7 +154,7 @@
stack[sp] = z
sp++
- case compile.UPLUS, compile.UMINUS, compile.TILDE:
+ case compile.UPLUS, compile.UMINUS, compile.USTAR, compile.TILDE:
var unop syntax.Token
if op == compile.TILDE {
unop = syntax.TILDE
@@ -241,6 +241,7 @@
}
if kwargs != nil {
// Add key/value items from **kwargs dictionary.
+ // TODO: support any enumerable mapping, such a Go map.
dict, ok := kwargs.(*Dict)
if !ok {
err = fmt.Errorf("argument after ** must be a mapping, not %s", kwargs.Type())
@@ -547,6 +548,36 @@
stack[sp] = Universe[f.Prog.Names[arg]]
sp++
+ case compile.ADDRESS:
+ x := stack[sp-1]
+ v, ok := x.(Variable)
+ if !ok {
+ err = fmt.Errorf("%s value has no address", x.Type())
+ break loop
+ }
+ stack[sp-1] = v.Address()
+
+ case compile.VALUE:
+ x := stack[sp-1]
+ if v, ok := x.(Variable); ok {
+ x = v.Value()
+ }
+ stack[sp-1] = x
+
+ case compile.SETVALUE:
+ y := stack[sp-1]
+ x := stack[sp-2]
+ sp -= 2
+ v, ok := x.(Variable)
+ if !ok {
+ err = fmt.Errorf("cannot set value of %s", x.Type())
+ break loop
+ }
+ if err2 := v.SetValue(y); err2 != nil {
+ err = err2
+ break loop
+ }
+
default:
err = fmt.Errorf("unimplemented: %s", op)
break loop
diff --git a/starlark/testdata/address.star b/starlark/testdata/address.star
new file mode 100644
index 0000000..057eab5
--- /dev/null
+++ b/starlark/testdata/address.star
@@ -0,0 +1,13 @@
+# Minimal tests of addressing operators.
+# See Stargo for more comprehensive tests,
+# including variables of aggregate (struct/array) type.
+# option:addressing
+
+load("assert.star", "assert")
+
+assert.eq(addr.v, None)
+ptr = &addr.v
+assert.eq(type(ptr), "pointer")
+*ptr = 2
+assert.eq(addr.v, 2)
+assert.eq((*ptr), 2) # parens are required
diff --git a/starlark/var.go b/starlark/var.go
new file mode 100644
index 0000000..7134fa0
--- /dev/null
+++ b/starlark/var.go
@@ -0,0 +1,81 @@
+package starlark
+
+// A Variable represents an addressable variable.
+//
+// Addressable variables are used only in Stargo,
+// which sets the resolve.AllowAddressing mode flag.
+//
+// An addressable variable supports three operations:
+// it contains a value, retrieved using the Value method;
+// it has an address (a value that denotes the variable's identity),
+// obtained using the Address method;
+// and its contents may be updated, using the SetValue method.
+//
+// Ordinary Starlark variables are not addressable, but they always
+// contain a reference. To perform an update such as x[i].f = 1, the
+// expression x[i] is evaluated, which yields a reference; then the
+// operation "set field .f to 1" is applied to the reference.
+// Similarly x.f[i] = 2 is executed by evaluating x.f to obtain a
+// reference, then applying the operation "set element i to 2" to it.
+// One may introduce a temporary variable for the reference without
+// changing the meaning of the statement, for example:
+// tmp = x.f; tmp[i] = 2.
+//
+// By contrast, in Go, an expression such as x[i].f, where x is variable
+// of type array-of-structs, has a dual meaning. When it appears in an
+// ordinary expression, it means: find the variable x[i].f within x and
+// retrieve its value. But when it appears on the left side of an
+// assignment, as in x[i].f = 1, it means find the variable x[i].f
+// within x and update its value. It cannot be decomposed into two
+// operations tmp = x[i]; tmp.f = 2 without changing its meaning because
+// the first operation would make a copy of the variable x[i] and the
+// second would mutate the copy, not the original. It can be
+// decomposed only by using pointers, for example:
+// ptr = &x[i]; ptr.f = 2.
+// A Go compiler implicitly does this decomposition using pointers when
+// it generates code for x[i].f on the left side of an assignment, or
+// as the operand of a &-operator. This is called "l-mode" (l for left),
+// opposed to r-mode code generation, in which the expression's value,
+// not its address, is needed.
+//
+// In order to support these operations on Go variables with the usual
+// semantics, in AllowAddressing mode the Starlark compiler, like a Go
+// compiler, generates different code for sequences of operations such
+// as x[i].f based on whether they appear on the left or right side of
+// an assignment (l-mode or r-mode).
+//
+// In both cases the compiler generates a sequence of calls to
+// Indexable.Index and HasAttrs.Attr for all but the last field/index
+// operation. The sequence is then followed by a call to one of the
+// following.
+// 1. Variable.Value, for an r-mode expression. If the operand is not a
+// Variable, it is assumed to be an ordinary Starlark value and is left
+// unchanged.
+// 2. SetIndexable.SetIndex, for an element update.
+// 3. HasSetField.SetField, for a field update;
+// 4. Variable.Address, for an explicit & operation.
+//
+// The *x operation may also yield a Variable (when x is a pointer), so
+// the Starlark compiler follows all *x operations by a call to
+// Variable.Value to yield the contents of the variable.
+//
+// Variables are technically Values but they are used only transiently
+// during one of the four above operations. They should generally not be
+// visible to Starlark programs. In Stargo, Variables used for globals
+// of a Go package such as http.DefaultServeMux are always accessed
+// using a dot expression.
+//
+// A concrete type that satisfies Variable could be represented by a
+// non-nil Go pointer, p: Address would return p; Value would return *p;
+// and SetValue(x) would execute *p=x. But the Variable is logically an
+// abstraction of the variable *p, not the pointer itself. An
+// alternative representation is reflect.Value, which is capable of
+// representing both ordinary values and addressable variables; see
+// reflect.Value.CanAddr.
+//
+type Variable interface {
+ Value
+ Address() Value
+ Value() Value
+ SetValue(Value) error
+}
diff --git a/syntax/parse.go b/syntax/parse.go
index af79cc1..8a6646c 100644
--- a/syntax/parse.go
+++ b/syntax/parse.go
@@ -590,7 +590,7 @@
// 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 -/+/&/*/~ have higher precedence so are handled in parsePrimary.
// See https://github.com/google/starlark-go/blob/master/doc/spec.md#binary-operators
var preclevels = [...][]Token{
{OR}, // or
@@ -749,7 +749,7 @@
// | '[' ... // list literal or comprehension
// | '{' ... // dict literal or comprehension
// | '(' ... // tuple or parenthesized expression
-// | ('-'|'+'|'~') primary_with_suffix
+// | ('-'|'+'|'~'|'&') primary_with_suffix
func (p *parser) parsePrimary() Expr {
switch p.tok {
case IDENT:
@@ -795,7 +795,7 @@
Rparen: rparen,
}
- case MINUS, PLUS, TILDE: // unary
+ case MINUS, PLUS, TILDE, AMP, STAR: // unary
tok := p.tok
pos := p.nextToken()
x := p.parsePrimaryWithSuffix()
diff --git a/syntax/parse_test.go b/syntax/parse_test.go
index e4cb9f4..ce272c4 100644
--- a/syntax/parse_test.go
+++ b/syntax/parse_test.go
@@ -106,6 +106,9 @@
`(BinaryExpr X=a Op=and Y=(UnaryExpr Op=not X=b))`},
{`[e for x in y if cond1 if cond2]`,
`(Comprehension Body=e Clauses=((ForClause Vars=x X=y) (IfClause Cond=cond1) (IfClause Cond=cond2)))`}, // github.com/google/skylark/issues/53
+ {`&a[i].f[j]`, `(UnaryExpr Op=& X=(IndexExpr X=(DotExpr X=(IndexExpr X=a Y=i) Name=f) Y=j))`},
+ {`&x + y`, `(BinaryExpr X=(UnaryExpr Op=& X=x) Op=+ Y=y)`},
+ {`&x * y`, `(BinaryExpr X=(UnaryExpr Op=& X=x) Op=* Y=y)`},
} {
e, err := syntax.ParseExpr("foo.star", test.input, 0)
if err != nil {
diff --git a/syntax/testdata/errors.star b/syntax/testdata/errors.star
index b7c9b3d..ee5a72d 100644
--- a/syntax/testdata/errors.star
+++ b/syntax/testdata/errors.star
@@ -8,7 +8,7 @@
---
-_ = *x ### `got '\*', want primary`
+_ = %x ### `got '%', want primary`
---