internal/core/compile: separate out phase of let clauses

This allows referring to lets later in scope, as the spec allows.

By hindsight, a better mechanism would be to treat lets as
arcs with special handling. This will preserve formatting
information and will obviate the need for special caching
to avoid exponential blowup.

This can be done as an overall rework of making Vertex
processing linear, instead of the current O(n^2).

Change-Id: I7b5f04c3fb434425d9f1c53fc4b1c1f2d218ed25
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6624
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
diff --git a/cue/testdata/compile/scope.txtar b/cue/testdata/compile/scope.txtar
index 094d82f..42a032e 100644
--- a/cue/testdata/compile/scope.txtar
+++ b/cue/testdata/compile/scope.txtar
@@ -29,6 +29,12 @@
 B = {open: int}
 f: B
 
+schema: {
+		next:  _schema_1
+}
+
+let _schema_1 = schema
+
 -- out/compile --
 --- in.cue
 {
@@ -64,9 +70,17 @@
     }
   }
   f: 〈0;let B〉
+  schema: {
+    next: 〈1;let _schema_1〉
+  }
 }
 -- out/eval --
-(struct){
+Errors:
+structural cycle
+
+Result:
+(_|_){
+  // [structural cycle]
   e: (struct){
   }
   a: (struct){
@@ -86,4 +100,12 @@
   f: (struct){
     open: (int){ int }
   }
+  schema: (_|_){
+    // [structural cycle]
+    next: (_|_){
+      // [structural cycle] structural cycle
+      next: (_|_){// 〈1;let _schema_1〉
+      }
+    }
+  }
 }
diff --git a/internal/core/compile/compile.go b/internal/core/compile/compile.go
index 0b85136..6a174f7 100644
--- a/internal/core/compile/compile.go
+++ b/internal/core/compile/compile.go
@@ -441,6 +441,9 @@
 
 func (c *compiler) addDecls(st *adt.StructLit, a []ast.Decl) {
 	for _, d := range a {
+		c.addLetDecl(d)
+	}
+	for _, d := range a {
 		if x := c.decl(d); x != nil {
 			st.Decls = append(st.Decls, x)
 		}
@@ -535,32 +538,8 @@
 			}
 		}
 
-	// An alias reference will have an expression that is looked up in the
-	// environment cash.
-	case *ast.LetClause:
-		// Cache the parsed expression. Creating a unique expression for each
-		// reference allows the computation to be shared given that we don't
-		// have fields for expressions. This, in turn, prevents exponential
-		// blowup. in x2: x1+x1, x3: x2+x2, ... patterns.
-
-		expr := c.labeledExpr(nil, (*letScope)(x), x.Expr)
-
-		a := aliasEntry{source: x, expr: expr}
-
-		if err := c.insertAlias(x.Ident, a); err != nil {
-			return err
-		}
-
-	case *ast.Alias:
-
-		expr := c.labeledExpr(nil, (*deprecatedAliasScope)(x), x.Expr)
-
-		// TODO(legacy): deprecated, remove this use of Alias
-		a := aliasEntry{source: x, expr: expr}
-
-		if err := c.insertAlias(x.Ident, a); err != nil {
-			return err
-		}
+	// Handled in addLetDecl.
+	case *ast.LetClause, *ast.Alias:
 
 	case *ast.CommentGroup:
 		// Nothing to do for a free-floating comment group.
@@ -586,6 +565,33 @@
 	return nil
 }
 
+func (c *compiler) addLetDecl(d ast.Decl) {
+	switch x := d.(type) {
+	// An alias reference will have an expression that is looked up in the
+	// environment cash.
+	case *ast.LetClause:
+		// Cache the parsed expression. Creating a unique expression for each
+		// reference allows the computation to be shared given that we don't
+		// have fields for expressions. This, in turn, prevents exponential
+		// blowup in x2: x1+x1, x3: x2+x2, ... patterns.
+
+		expr := c.labeledExpr(nil, (*letScope)(x), x.Expr)
+
+		a := aliasEntry{source: x, expr: expr}
+
+		c.insertAlias(x.Ident, a)
+
+	case *ast.Alias:
+
+		expr := c.labeledExpr(nil, (*deprecatedAliasScope)(x), x.Expr)
+
+		// TODO(legacy): deprecated, remove this use of Alias
+		a := aliasEntry{source: x, expr: expr}
+
+		c.insertAlias(x.Ident, a)
+	}
+}
+
 func (c *compiler) elem(n ast.Expr) adt.Elem {
 	switch x := n.(type) {
 	case *ast.Ellipsis:
diff --git a/internal/core/eval/eval_test.go b/internal/core/eval/eval_test.go
index 99601b8..34fded1 100644
--- a/internal/core/eval/eval_test.go
+++ b/internal/core/eval/eval_test.go
@@ -100,6 +100,7 @@
 	"export/030": "cycle",
 
 	"cycle/025_cannot_resolve_references_that_would_be_ambiguous": "cycle",
+	"compile/scope": "cycle",
 
 	"resolve/034_closing_structs": "close()",
 	"fulleval/053_issue312":       "close()",