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`
 
 ---