cue: introduce let declaration

This only implements let as a top-level declaration;
it does not yet implement let expressions in
comprehensions.

This is the first step to deprecate aliases of the form `X=foo`
The meaning of `=` is different in all other cases and is
confusing. Also, after some time of deprecating the old
aliases, it can be reintroduced as an alias for embedding.

This change does not yet do the automated rewriting.

Issue #339

Change-Id: Iaf6f9bf09e1bef7e18c35b776e7d415f526a02cc
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5684
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go
index 06e07fe..9388ac1 100644
--- a/cmd/cue/cmd/import.go
+++ b/cmd/cue/cmd/import.go
@@ -478,6 +478,8 @@
 			name, _, _ = ast.LabelName(x.Label)
 		case *ast.Alias:
 			name = x.Ident.Name
+		case *ast.LetClause:
+			name = x.Ident.Name
 		}
 		if name != "" {
 			h.fields[name] = true
@@ -528,7 +530,7 @@
 				ast.NewIdent(dataField))
 
 			// TODO: use definitions instead
-			c.InsertAfter(astutil.ApplyRecursively(&ast.Alias{
+			c.InsertAfter(astutil.ApplyRecursively(&ast.LetClause{
 				Ident: ast.NewIdent(dataField),
 				Expr:  expr,
 			}))
diff --git a/cmd/cue/cmd/testdata/script/import_hoiststr.txt b/cmd/cue/cmd/testdata/script/import_hoiststr.txt
index 5172148..d45414c 100644
--- a/cmd/cue/cmd/testdata/script/import_hoiststr.txt
+++ b/cmd/cue/cmd/testdata/script/import_hoiststr.txt
@@ -18,7 +18,7 @@
 		foo
 		"""
 		json: json656e63.Marshal(_cue_json)
-		_cue_json = [1, 2]
+		let _cue_json = [1, 2]
 	}]
 }
 deployment: booster: [{
diff --git a/cue/ast.go b/cue/ast.go
index 04058b2..bc5b61c 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -275,7 +275,7 @@
 				// Solving this is best done with a generic topological sort
 				// mechanism.
 
-			case *ast.Field, *ast.Alias:
+			case *ast.Field, *ast.Alias, *ast.LetClause:
 				v1.walk(e)
 
 			case *ast.Comprehension:
@@ -487,7 +487,7 @@
 			panic("cue: unknown label type")
 		}
 
-	case *ast.Alias:
+	case *ast.Alias, *ast.LetClause:
 		// parsed verbatim at reference.
 
 	case *ast.ListComprehension:
@@ -551,7 +551,8 @@
 		}
 
 		// Type of reference      Scope          Node
-		// Alias declaration      File/Struct    Alias
+		// Let Clause             File/Struct    LetClause
+		// Alias declaration      File/Struct    Alias (deprecated)
 		// Illegal Reference      File/Struct
 		// Fields
 		//    Label               File/Struct    ParenExpr, Ident, BasicLit
@@ -561,17 +562,24 @@
 		//    Label               Field          Expr
 		//    Value               Field          Field
 		// Pkg                    nil            ImportSpec
+		var expr ast.Expr
+		switch x := n.Node.(type) {
+		case *ast.Alias:
+			expr = x.Expr
+		case *ast.LetClause:
+			expr = x.Expr
+		}
 
-		if x, ok := n.Node.(*ast.Alias); ok {
+		if expr != nil {
 			// TODO(lang): should we exempt definitions? The substitution
 			// principle says we should not.
-			if ret = v.aliasMap[x.Expr]; ret != nil {
+			if ret = v.aliasMap[expr]; ret != nil {
 				break
 			}
 			old := v.ctx().inDefinition
 			v.ctx().inDefinition = 0
-			ret = v.walk(x.Expr)
-			v.aliasMap[x.Expr] = ret
+			ret = v.walk(expr)
+			v.aliasMap[expr] = ret
 			v.ctx().inDefinition = old
 			break
 		}
diff --git a/cue/ast/ast.go b/cue/ast/ast.go
index c0dcc1c..e058394 100644
--- a/cue/ast/ast.go
+++ b/cue/ast/ast.go
@@ -614,6 +614,18 @@
 	clause
 }
 
+// A LetClause node represents a let clause in a comprehension.
+type LetClause struct {
+	Let   token.Pos
+	Ident *Ident
+	Equal token.Pos
+	Expr  Expr
+
+	comments
+	clause
+	decl
+}
+
 // A ParenExpr node represents a parenthesized expression.
 type ParenExpr struct {
 	Lparen token.Pos // position of "("
@@ -744,6 +756,8 @@
 func (x *Ellipsis) pos() *token.Pos          { return &x.Ellipsis }
 func (x *ListComprehension) Pos() token.Pos  { return x.Lbrack }
 func (x *ListComprehension) pos() *token.Pos { return &x.Lbrack }
+func (x *LetClause) Pos() token.Pos          { return x.Let }
+func (x *LetClause) pos() *token.Pos         { return &x.Let }
 func (x *ForClause) Pos() token.Pos          { return x.For }
 func (x *ForClause) pos() *token.Pos         { return &x.For }
 func (x *IfClause) Pos() token.Pos           { return x.If }
@@ -787,6 +801,7 @@
 	return x.Ellipsis.Add(3) // len("...")
 }
 func (x *ListComprehension) End() token.Pos { return x.Rbrack }
+func (x *LetClause) End() token.Pos         { return x.Expr.End() }
 func (x *ForClause) End() token.Pos         { return x.Source.End() }
 func (x *IfClause) End() token.Pos          { return x.Condition.End() }
 func (x *ParenExpr) End() token.Pos         { return x.Rparen.Add(1) }
diff --git a/cue/ast/astutil/apply.go b/cue/ast/astutil/apply.go
index 05954d2..fe36a9c 100644
--- a/cue/ast/astutil/apply.go
+++ b/cue/ast/astutil/apply.go
@@ -461,6 +461,10 @@
 	case *ast.EmbedDecl:
 		apply(v, c, &n.Expr)
 
+	case *ast.LetClause:
+		apply(v, c, &n.Ident)
+		apply(v, c, &n.Expr)
+
 	case *ast.Alias:
 		apply(v, c, &n.Ident)
 		apply(v, c, &n.Expr)
diff --git a/cue/ast/astutil/resolve.go b/cue/ast/astutil/resolve.go
index 9195ca9..84fcb90 100644
--- a/cue/ast/astutil/resolve.go
+++ b/cue/ast/astutil/resolve.go
@@ -94,6 +94,11 @@
 			if isIdent {
 				s.insert(name, x.Value)
 			}
+		case *ast.LetClause:
+			name, isIdent, _ := ast.LabelName(x.Ident)
+			if isIdent {
+				s.insert(name, x)
+			}
 		case *ast.Alias:
 			name, isIdent, _ := ast.LabelName(x.Ident)
 			if isIdent {
@@ -105,11 +110,14 @@
 	return s
 }
 
-func (s *scope) isAlias(n ast.Node) bool {
+func (s *scope) isLet(n ast.Node) bool {
 	if _, ok := s.node.(*ast.Field); ok {
 		return true
 	}
 	switch n.(type) {
+	case *ast.LetClause:
+		return true
+
 	case *ast.Alias:
 		return true
 
@@ -125,8 +133,8 @@
 	}
 	// TODO: record both positions.
 	if outer, _, existing := s.lookup(name); existing != nil {
-		isAlias1 := s.isAlias(n)
-		isAlias2 := outer.isAlias(existing)
+		isAlias1 := s.isLet(n)
+		isAlias2 := outer.isLet(existing)
 		if isAlias1 != isAlias2 {
 			s.errFn(n.Pos(), "cannot have both alias and field with name %q in same scope", name)
 			return
@@ -261,6 +269,18 @@
 
 		return nil
 
+	case *ast.LetClause:
+		// Disallow referring to the current LHS name.
+		name := x.Ident.Name
+		saved := s.index[name]
+		delete(s.index, name) // The same name may still appear in another scope
+
+		if x.Expr != nil {
+			walk(s, x.Expr)
+		}
+		s.index[name] = saved
+		return nil
+
 	case *ast.Alias:
 		// Disallow referring to the current LHS name.
 		name := x.Ident.Name
diff --git a/cue/ast/astutil/walk.go b/cue/ast/astutil/walk.go
index 6a24234..269e371 100644
--- a/cue/ast/astutil/walk.go
+++ b/cue/ast/astutil/walk.go
@@ -183,6 +183,10 @@
 			walk(v, c)
 		}
 
+	case *ast.LetClause:
+		walk(v, n.Ident)
+		walk(v, n.Expr)
+
 	case *ast.ForClause:
 		if n.Key != nil {
 			walk(v, n.Key)
diff --git a/cue/ast/walk.go b/cue/ast/walk.go
index 85a45fc..659d6c4 100644
--- a/cue/ast/walk.go
+++ b/cue/ast/walk.go
@@ -165,6 +165,10 @@
 	case *EmbedDecl:
 		walk(v, n.Expr)
 
+	case *LetClause:
+		walk(v, n.Ident)
+		walk(v, n.Expr)
+
 	case *Alias:
 		walk(v, n.Ident)
 		walk(v, n.Expr)
diff --git a/cue/ast_test.go b/cue/ast_test.go
index cb2e519..4faceff 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -170,10 +170,10 @@
 	}, {
 		// bunch of aliases
 		in: `
-		a1 = a2
-		a2 = 5
+		let a1 = a2
+		let a2 = 5
 		b: a1
-		a3 = d
+		let a3 = d
 		c: {
 			d: {
 				r: a3
@@ -186,11 +186,11 @@
 	}, {
 		// aliases with errors
 		in: `
-		e1 = 1
-		e1 = 2
+		let e1 = 1
+		let e1 = 2
 		e1v: e1
 		e2: "a"
-		e2 = "a"
+		let e2 = "a"
 		`,
 		out: `alias "e1" redeclared in same scope:` + "\n" +
 			"    test:3:3\n" +
@@ -199,7 +199,7 @@
 			"<0>{}",
 	}, {
 		in: `
-		a = b
+		let a = b
 		b: {
 			c: a // reference to own root.
 		}
@@ -271,7 +271,7 @@
 		[X=string]: int
 		X=[string]: int
 		Y=foo: int
-		Y=3
+		let Y=3
 		Z=[string]: { Z=3, a: int } // allowed
 		`,
 		out: `alias "X" redeclared in same scope:
diff --git a/cue/export.go b/cue/export.go
index b666f0f..75f3690 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -85,7 +85,7 @@
 				ident = ast.NewIdent(info.name)
 			}
 			if info.alias != "" {
-				file.Decls = append(file.Decls, &ast.Alias{
+				file.Decls = append(file.Decls, &ast.LetClause{
 					Ident: ast.NewIdent(info.alias),
 					Expr:  ast.NewIdent(info.short),
 				})
diff --git a/cue/export_test.go b/cue/export_test.go
index 3c1a98c..7e7fb1c 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -666,7 +666,7 @@
 		in: `
 		import "strings"
 
-		stringsx = strings
+		let stringsx = strings
 
 		a: {
 			strings: stringsx.ContainsAny("c")
@@ -675,7 +675,7 @@
 		out: unindent(`
 		import "strings"
 
-		STRINGS = strings
+		let STRINGS = strings
 		a: strings: STRINGS.ContainsAny("c")`),
 	}, {
 		in: `
diff --git a/cue/format/format_test.go b/cue/format/format_test.go
index c596c36..b0da92e 100644
--- a/cue/format/format_test.go
+++ b/cue/format/format_test.go
@@ -355,8 +355,8 @@
 	"regexp"
 )
 
-pi = 3.14  // TODO: allow on same line
-xx = 0
+let pi = 3.14
+let xx = 0
 t: {
 	x: int
 	y: int
@@ -429,7 +429,7 @@
 
 var decls = []string{
 	"package p\n\n" + `import "fmt"`,
-	"package p\n\n" + "pi = 3.1415\ne = 2.71828\n\nx = pi",
+	"package p\n\n" + "let pi = 3.1415\nlet e = 2.71828\n\nlet x = pi",
 }
 
 func TestDeclLists(t *testing.T) {
diff --git a/cue/format/node.go b/cue/format/node.go
index 66a7d83..9cdd869 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -97,6 +97,8 @@
 		return len(x.Label.Comments()) > 0
 	case *ast.Alias:
 		return len(x.Ident.Comments()) > 0
+	case *ast.LetClause:
+		return len(x.Ident.Comments()) > 0
 	}
 	return false
 }
@@ -364,6 +366,16 @@
 		}
 		f.print(newsection, nooverride)
 
+	case *ast.LetClause:
+		if !decl.Pos().HasRelPos() || decl.Pos().RelPos() >= token.Newline {
+			f.print(formfeed)
+		}
+		f.print(n.Let, token.LET, blank, nooverride)
+		f.expr(n.Ident)
+		f.print(blank, nooverride, n.Equal, token.BIND, blank)
+		f.expr(n.Expr)
+		f.print(declcomma) // implied
+
 	case *ast.EmbedDecl:
 		if !n.Pos().HasRelPos() || n.Pos().RelPos() >= token.Newline {
 			f.print(formfeed)
diff --git a/cue/format/testdata/expressions.golden b/cue/format/testdata/expressions.golden
index 019abde..b5fd507 100644
--- a/cue/format/testdata/expressions.golden
+++ b/cue/format/testdata/expressions.golden
@@ -11,11 +11,11 @@
 	c: b: a:       4
 	c?: bb?: aaa?: 5
 	c: b: [Name=string]: a: int
-	alias = 3.14
+	let alias = 3.14
 	"g\("en")"?: 4
 
-	alias2 = foo // with comment
-	aaalias = foo
+	let alias2 = foo // with comment
+	let aaalias = foo
 	b: bar
 
 	bottom: _|_
diff --git a/cue/format/testdata/expressions.input b/cue/format/testdata/expressions.input
index d153771..b2974d4 100644
--- a/cue/format/testdata/expressions.input
+++ b/cue/format/testdata/expressions.input
@@ -11,11 +11,11 @@
     c: b: a:  4
     c?: bb?: aaa?: 5
     c: b: [Name=string]: a: int
-    alias = 3.14
+    let alias = 3.14
     "g\("en")"?: 4
 
-    alias2 = foo // with comment
-    aaalias = foo
+    let alias2 = foo // with comment
+    let aaalias = foo
     b: bar
 
     bottom: _|_
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index d9ea14c..e7409da 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -701,6 +701,9 @@
 		case token.FOR, token.IF:
 			list = append(list, p.parseComprehension())
 
+		case token.LET:
+			list = append(list, p.parseLetDecl())
+
 		case token.ATTRIBUTE:
 			list = append(list, p.parseAttribute())
 			if p.atComma("struct literal", token.RBRACE) { // TODO: may be EOF
@@ -724,7 +727,7 @@
 	return
 }
 
-func (p *parser) parseComprehension() (decl ast.Decl) {
+func (p *parser) parseLetDecl() (decl ast.Decl) {
 	if p.trace {
 		defer un(trace(p, "Field"))
 	}
@@ -732,6 +735,35 @@
 	c := p.openComments()
 	defer func() { c.closeNode(p, decl) }()
 
+	letPos := p.expect(token.LET)
+	if p.tok != token.IDENT {
+		return &ast.Ident{
+			NamePos: letPos,
+			Name:    "let",
+		}
+	}
+
+	ident := p.parseIdent()
+	assign := p.expect(token.BIND)
+	expr := p.parseRHS()
+	p.expectComma()
+
+	return &ast.LetClause{
+		Let:   letPos,
+		Ident: ident,
+		Equal: assign,
+		Expr:  expr,
+	}
+}
+
+func (p *parser) parseComprehension() (decl ast.Decl) {
+	if p.trace {
+		defer un(trace(p, "Comprehension"))
+	}
+
+	c := p.openComments()
+	defer func() { c.closeNode(p, decl) }()
+
 	tok := p.tok
 	pos := p.pos
 	clauses, fc := p.parseComprehensionClauses(true)
@@ -862,6 +894,8 @@
 		case token.RBRACE, token.EOF:
 			if i == 0 {
 				if a, ok := expr.(*ast.Alias); ok {
+					p.assertV0(p.pos, 1, 3, `old-style alias; use "let X = expr"`)
+
 					return a
 				}
 				switch tok {
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 7e4f61b..2142dca 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -254,6 +254,14 @@
 			 }`,
 		`{y: {a: 1, b: 2}, a: {for k: v in y if v>2 {"\(k)": v}}}`,
 	}, {
+		"let declaration",
+		`{
+			let X = 42
+			let Y = "42",
+			let Z = 10 + 12
+		}`,
+		`{let X=42, let Y="42", let Z=10+12}`,
+	}, {
 		"duplicates allowed",
 		`{
 			a: b: 3
diff --git a/cue/parser/print.go b/cue/parser/print.go
index b851505..95f2d5c 100644
--- a/cue/parser/print.go
+++ b/cue/parser/print.go
@@ -49,6 +49,13 @@
 		out += debugStr(v.Name)
 		return out
 
+	case *ast.LetClause:
+		out := "let "
+		out += debugStr(v.Ident)
+		out += "="
+		out += debugStr(v.Expr)
+		return out
+
 	case *ast.Alias:
 		out := debugStr(v.Ident)
 		out += "="
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 7126bf3..d8e95b9 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -2866,17 +2866,17 @@
 		desc: "alias reuse in nested scope",
 		in: `
 		Foo :: {
-			X = or([ for k, _ in {} { k } ])
+			let X = or([ for k, _ in {} { k } ])
 			connection: [X]: X
 		}
 		A :: {
 			foo: "key"
-			X = foo
+			let X = foo
 			a: foo: [X]: X
 		}
 		B :: {
 			foo: string
-			X = foo
+			let X = foo
 			a: foo: [X]: X
 		}
 		b: B & { foo: "key" }
diff --git a/doc/tutorial/basics/4_references/30_aliases.txt b/doc/tutorial/basics/4_references/30_aliases.txt
index e3af18e..8bf548e 100644
--- a/doc/tutorial/basics/4_references/30_aliases.txt
+++ b/doc/tutorial/basics/4_references/30_aliases.txt
@@ -14,7 +14,7 @@
 struct, and they do not appear in the output.
 
 -- alias.cue --
-A = a  // A is an alias for a
+let A = a  // A is an alias for a
 a: {
     d: 3
 }
diff --git a/doc/tutorial/kubernetes/README.md b/doc/tutorial/kubernetes/README.md
index 4c998e1..28178fa 100644
--- a/doc/tutorial/kubernetes/README.md
+++ b/doc/tutorial/kubernetes/README.md
@@ -502,7 +502,7 @@
             for c in v.spec.template.spec.containers
             for p in c.ports
             if p._export {
-                Port = p.containerPort // Port is an alias
+                let Port = p.containerPort // Port is an alias
                 port:       *Port | int
                 targetPort: *Port | int
             }  
diff --git a/doc/tutorial/kubernetes/quick/services/kube.cue b/doc/tutorial/kubernetes/quick/services/kube.cue
index 394b15e..fee8edb 100644
--- a/doc/tutorial/kubernetes/quick/services/kube.cue
+++ b/doc/tutorial/kubernetes/quick/services/kube.cue
@@ -99,7 +99,7 @@
 			for c in v.spec.template.spec.containers
 			for p in c.ports
 			if p._export {
-				Port = p.containerPort // Port is an alias
+				let Port = p.containerPort // Port is an alias
 				port:       *Port | int
 				targetPort: *Port | int
 			},
diff --git a/doc/tutorial/kubernetes/quick/services/mon/alertmanager/configmap.cue b/doc/tutorial/kubernetes/quick/services/mon/alertmanager/configmap.cue
index ad04bc9..5f4360e 100644
--- a/doc/tutorial/kubernetes/quick/services/mon/alertmanager/configmap.cue
+++ b/doc/tutorial/kubernetes/quick/services/mon/alertmanager/configmap.cue
@@ -7,7 +7,7 @@
 	kind:       "ConfigMap"
 	data: {
 		"alerts.yaml": yaml656e63.Marshal(_cue_alerts_yaml)
-		_cue_alerts_yaml = {
+		let _cue_alerts_yaml = {
 			receivers: [{
 				name: "pager"
 				// email_configs:
diff --git a/doc/tutorial/kubernetes/quick/services/mon/prometheus/configmap.cue b/doc/tutorial/kubernetes/quick/services/mon/prometheus/configmap.cue
index 814b924..835d787 100644
--- a/doc/tutorial/kubernetes/quick/services/mon/prometheus/configmap.cue
+++ b/doc/tutorial/kubernetes/quick/services/mon/prometheus/configmap.cue
@@ -7,7 +7,7 @@
 	kind:       "ConfigMap"
 	data: {
 		"alert.rules": yaml656e63.Marshal(_cue_alert_rules)
-		_cue_alert_rules = {
+		let _cue_alert_rules = {
 			groups: [{
 				name: "rules.yaml"
 				rules: [{
@@ -49,7 +49,7 @@
 		}
 
 		"prometheus.yml": yaml656e63.Marshal(_cue_prometheus_yml)
-		_cue_prometheus_yml = {
+		let _cue_prometheus_yml = {
 			global: scrape_interval: "15s"
 			rule_files: [
 				"/etc/prometheus/alert.rules",
diff --git a/encoding/openapi/testdata/builtins.cue b/encoding/openapi/testdata/builtins.cue
index 8a7f0a1..4d1b041 100644
--- a/encoding/openapi/testdata/builtins.cue
+++ b/encoding/openapi/testdata/builtins.cue
@@ -3,7 +3,7 @@
 	"list"
 )
 
-_time = time
+let _time = time
 
 MyStruct :: {
 	timestamp1?: time.Time
diff --git a/encoding/protobuf/parse.go b/encoding/protobuf/parse.go
index 9cb342b..7274b00 100644
--- a/encoding/protobuf/parse.go
+++ b/encoding/protobuf/parse.go
@@ -277,7 +277,7 @@
 			if !ok {
 				// TODO: this is likely to be okay, but find something better.
 				alias = "_" + first + "_"
-				p.file.Decls = append(p.file.Decls, &ast.Alias{
+				p.file.Decls = append(p.file.Decls, &ast.LetClause{
 					Ident: ast.NewIdent(alias),
 					Expr:  ast.NewIdent(first),
 				})
diff --git a/encoding/protobuf/testdata/attributes.proto.out.cue b/encoding/protobuf/testdata/attributes.proto.out.cue
index e9fd95d..d0f6618 100644
--- a/encoding/protobuf/testdata/attributes.proto.out.cue
+++ b/encoding/protobuf/testdata/attributes.proto.out.cue
@@ -145,8 +145,8 @@
 		[string]: StringMap
 	} @protobuf(9,type=map<sint32,StringMap>,string_maps,"(gogoproto.nullable)=false")
 }
-_time_ = time
-_bytes_ = bytes
+let _time_ = time
+let _bytes_ = bytes
 
 // A map of string to string. The keys and values in this map are dictionary
 // indices (see the [Attributes][istio.mixer.v1.CompressedAttributes] message for an explanation)
diff --git a/encoding/protobuf/testdata/istio.io/api/mixer/v1/attributes_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/mixer/v1/attributes_proto_gen.cue
index e9fd95d..d0f6618 100644
--- a/encoding/protobuf/testdata/istio.io/api/mixer/v1/attributes_proto_gen.cue
+++ b/encoding/protobuf/testdata/istio.io/api/mixer/v1/attributes_proto_gen.cue
@@ -145,8 +145,8 @@
 		[string]: StringMap
 	} @protobuf(9,type=map<sint32,StringMap>,string_maps,"(gogoproto.nullable)=false")
 }
-_time_ = time
-_bytes_ = bytes
+let _time_ = time
+let _bytes_ = bytes
 
 // A map of string to string. The keys and values in this map are dictionary
 // indices (see the [Attributes][istio.mixer.v1.CompressedAttributes] message for an explanation)
diff --git a/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue
index cc2893f..1da46a3 100644
--- a/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue
+++ b/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue
@@ -98,7 +98,7 @@
 		[string]: QuotaResult
 	} @protobuf(3,type=map<string,QuotaResult>,"(gogoproto.nullable)=false")
 }
-_status_ = status
+let _status_ = status
 
 // Describes the attributes that were used to determine the response.
 // This can be used to construct a response cache.
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
index e720284..379e378 100644
--- a/internal/encoding/encoding.go
+++ b/internal/encoding/encoding.go
@@ -395,7 +395,7 @@
 	case *ast.Ellipsis:
 		check(n, constraints, "ellipsis", true)
 
-	case *ast.Ident, *ast.SelectorExpr, *ast.Alias:
+	case *ast.Ident, *ast.SelectorExpr, *ast.Alias, *ast.LetClause:
 		check(n, i.References, "references", true)
 
 	default: