cue/parser: add support for failing on legacy grammar

Implement test in tutorial to detect legacy usage.

Issue #87

Change-Id: Id2aa0e928029b199acc7a9d77e38402a348f61d4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3549
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/parser/interface.go b/cue/parser/interface.go
index 25b5af3..ce09af3 100644
--- a/cue/parser/interface.go
+++ b/cue/parser/interface.go
@@ -71,6 +71,28 @@
 	}
 )
 
+// FromVersion specifies until which legacy version the parser should provide
+// backwards compatibility.
+func FromVersion(version int) Option {
+	if version >= 0 {
+		version++
+	}
+	// Versions:
+	// <0:  major version 0 (counting -1000 + x, where x = 100*m+p in 0.m.p
+	// >=0: x+1 in 1.x.y
+	return func(p *parser) { p.version = version }
+}
+
+func version0(minor, patch int) int {
+	return -1000 + 100*minor + patch
+}
+
+// Latest specifies the latest version of the parser, effectively setting
+// the strictest implementation.
+const Latest = latest
+
+const latest = 1000
+
 // FileOffset specifies the File position info to use.
 func FileOffset(pos int) Option {
 	return func(p *parser) { p.offset = pos }
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 2e99cb1..894e2a7 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -60,6 +60,7 @@
 
 	imports []*ast.ImportSpec // list of imports
 
+	version int
 }
 
 func (p *parser) init(filename string, src []byte, mode []Option) {
@@ -255,6 +256,8 @@
 	// Scan the comment for '\n' chars and adjust endline accordingly.
 	endline = p.file.Line(p.pos)
 	if p.lit[1] == '*' {
+		p.assertV0(0, 10, "block quotes")
+
 		// don't use range here - no need to decode Unicode code points
 		for i := 0; i < len(p.lit); i++ {
 			if p.lit[i] == '\n' {
@@ -346,6 +349,12 @@
 	}
 }
 
+func (p *parser) assertV0(minor, patch int, name string) {
+	if p.version != 0 && p.version > version0(minor, patch) {
+		p.errf(p.pos, "%s deprecated as of v0.%d.%d", name, minor, patch+1)
+	}
+}
+
 func (p *parser) errf(pos token.Pos, msg string, args ...interface{}) {
 	// ePos := p.file.Position(pos)
 	ePos := pos
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index 79bf9a8..85e56c9 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -408,6 +408,22 @@
 	}
 }
 
+func TestStrict(t *testing.T) {
+	testCases := []struct{ desc, in string }{
+		{"block comments",
+			`a: 1 /* a */`},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			mode := []Option{AllErrors, ParseComments, FromVersion(Latest)}
+			_, err := ParseFile("input", tc.in, mode...)
+			if err == nil {
+				t.Errorf("unexpected success: %v", tc.in)
+			}
+		})
+	}
+}
+
 func TestParseExpr(t *testing.T) {
 	// just kicking the tires:
 	// a valid arithmetic expression
diff --git a/cue/value.go b/cue/value.go
index d1a1b1d..519408b 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -76,7 +76,7 @@
 	}
 	got := x.kind()
 	if got&want&concreteKind == bottomKind && want != bottomKind {
-		return ctx.mkErr(x, "cannot use value %v (type %s) as %s", x, got, want)
+		return ctx.mkErr(x, "cannot use value %v (type %s) as %s", ctx.str(x), got, want)
 	}
 	if !got.isGround() {
 		return ctx.mkErr(x, codeIncomplete,
diff --git a/doc/tutorial/basics/script_test.go b/doc/tutorial/basics/script_test.go
index 92e2179..6b68152 100644
--- a/doc/tutorial/basics/script_test.go
+++ b/doc/tutorial/basics/script_test.go
@@ -3,15 +3,49 @@
 import (
 	"flag"
 	"os"
+	"path"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"cuelang.org/go/cmd/cue/cmd"
+	"cuelang.org/go/cue/parser"
 	"github.com/rogpeppe/testscript"
+	"github.com/rogpeppe/testscript/txtar"
 )
 
 var update = flag.Bool("update", false, "update the test files")
 
+// TestLatest checks that the examples match the latest language standard,
+// even if still valid in backwards compatibility mode.
+func TestLatest(t *testing.T) {
+	filepath.Walk(".", func(fullpath string, info os.FileInfo, err error) error {
+		if !strings.HasSuffix(fullpath, ".txt") {
+			return nil
+		}
+
+		a, err := txtar.ParseFile(fullpath)
+		if err != nil {
+			t.Error(err)
+			return nil
+		}
+
+		for _, f := range a.Files {
+			t.Run(path.Join(fullpath, f.Name), func(t *testing.T) {
+				if !strings.HasSuffix(f.Name, ".cue") {
+					return
+				}
+				v := parser.FromVersion(parser.Latest)
+				_, err := parser.ParseFile(f.Name, f.Data, v)
+				if err != nil {
+					t.Errorf("%v: %v", fullpath, err)
+				}
+			})
+		}
+		return nil
+	})
+}
+
 func TestScript(t *testing.T) {
 	filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
 		if !info.IsDir() {