cue: use 0o notation for octal numbers

instead of Go style octals

Fixes cuelang/cue#4.

Also fixes bug where `0` is interpreted as octal, and
thus only has type int and not float as well

Change-Id: I038bfdd02cecd2c78c8151530d9b8ef2b16e9a8c
diff --git a/cue/lit.go b/cue/lit.go
index 21f1829..7632f9a 100644
--- a/cue/lit.go
+++ b/cue/lit.go
@@ -557,33 +557,30 @@
 				// only scanned "0x" or "0X"
 				return p.error(p.node, "illegal hexadecimal number %q", p.src)
 			}
-		} else if p.ch == 'b' || p.ch == 'B' {
+		} else if p.ch == 'b' {
 			base = 2
 			// binary int
 			p.next()
 			p.scanMantissa(2)
 			if p.p <= 2 {
-				// only scanned "0b" or "0B"
+				// only scanned "0b"
 				return p.error(p.node, "illegal binary number %q", p.src)
 			}
-		} else {
+		} else if p.ch == 'o' {
 			base = 8
-			// octal int or float
-			seenDecimalDigit := false
+			// octal int
+			p.next()
 			p.scanMantissa(8)
-			if p.ch == '8' || p.ch == '9' {
-				// illegal octal int or float
-				seenDecimalDigit = true
-				p.scanMantissa(10)
+			if p.p <= 2 {
+				// only scanned "0o"
+				return p.error(p.node, "illegal octal number %q", p.src)
 			}
-			// TODO: disallow complex
+		} else {
+			// int or float
+			p.scanMantissa(10)
 			if p.ch == '.' || p.ch == 'e' {
 				goto fraction
 			}
-			// octal int
-			if seenDecimalDigit {
-				return p.error(p.node, "illegal octal number %q", p.src)
-			}
 		}
 		goto exit
 	}
diff --git a/cue/lit_test.go b/cue/lit_test.go
index 4ed82fc..6287bfa 100644
--- a/cue/lit_test.go
+++ b/cue/lit_test.go
@@ -126,6 +126,7 @@
 		lit  string
 		node value
 	}{
+		{"0", mkInt(0)},
 		{"null", nullSentinel},
 		{"true", trueSentinel},
 		{"false", falseSentinel},
@@ -153,6 +154,7 @@
 		{"1", mkInt(1)},
 		{"100_000", hk},
 		{"1.", mkFloat("1")},
+		{"0.0", mkFloat("0.0")},
 		{".0", mkFloat(".0")},
 		{"1K", mkMul(1000, mulK, 10)},
 		{"1Mi", mkMul(1024*1024, mulMi, 10)},
@@ -166,7 +168,7 @@
 		{"0xABCD", mkMul(0xABCD, 0, 16)},
 		{"0b11001000", mkMul(0xc8, 0, 2)},
 		{"0b1", mkMul(1, 0, 2)},
-		{"0755", mkMul(0755, 0, 8)},
+		{"0o755", mkMul(0755, 0, 8)},
 	}
 	p := litParser{
 		ctx: &context{Context: &apd.BaseContext},
@@ -195,7 +197,7 @@
 		// not allowed in string literal, only binary
 		{`"foo\x00"`},
 		{`0x`},
-		{`09`},
+		{`0o`},
 		{`0_`},
 		{``},
 		{`"`},
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index 89493e5..f5f0881 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -324,30 +324,35 @@
 				// only scanned "0x" or "0X"
 				s.error(offs, "illegal hexadecimal number")
 			}
-		} else if s.ch == 'b' || s.ch == 'B' {
+		} else if s.ch == 'b' {
 			// binary int
 			s.next()
 			s.scanMantissa(2)
 			if s.offset-offs <= 2 {
-				// only scanned "0b" or "0B"
+				// only scanned "0b"
 				s.error(offs, "illegal binary number")
 			}
-		} else {
-			// octal int or float
-			seenDecimalDigit := false
+		} else if s.ch == 'o' {
+			// octal int
+			s.next()
 			s.scanMantissa(8)
-			if s.ch == '8' || s.ch == '9' {
-				// illegal octal int or float
-				seenDecimalDigit = true
+			if s.offset-offs <= 2 {
+				// only scanned "0o"
+				s.error(offs, "illegal octal number")
+			}
+		} else {
+			// 0 or float
+			seenDigits := false
+			if s.ch >= '0' && s.ch <= '9' {
+				seenDigits = true
 				s.scanMantissa(10)
 			}
-			// TODO: disallow complex.
 			if s.ch == '.' || s.ch == 'e' {
 				goto fraction
 			}
-			// octal int
-			if seenDecimalDigit {
-				s.error(offs, "illegal octal number")
+			if seenDigits {
+				// integer other than 0 may not start with 0
+				s.error(offs, "illegal integer number")
 			}
 		}
 		goto exit
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 2faf296..45e73a5 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -79,11 +79,12 @@
 	{token.INT, "12345_67890_12345_6788_90", literal},
 	{token.INT, "1234567M", literal},
 	{token.INT, "1234567Mi", literal},
-	{token.INT, "01234567", literal},
+	{token.INT, "1234567", literal},
 	{token.INT, ".3Mi", literal},
 	{token.INT, "3.3Mi", literal},
 	{token.INT, "0xcafebabe", literal},
 	{token.INT, "0b1100_1001", literal},
+	{token.INT, "0o1234567", literal},
 	{token.FLOAT, "0.", literal},
 	{token.FLOAT, ".0", literal},
 	{token.FLOAT, "3.14159265", literal},
@@ -758,17 +759,18 @@
 	{"'", token.STRING, 0, "'", "string literal not terminated"},
 	{"/**/", token.COMMENT, 0, "/**/", ""},
 	{"/*", token.COMMENT, 0, "/*", "comment not terminated"},
-	{"077", token.INT, 0, "077", ""},
+	{"0", token.INT, 0, "0", ""},
+	{"077", token.INT, 0, "077", "illegal integer number"},
 	{"078.", token.FLOAT, 0, "078.", ""},
 	{"07801234567.", token.FLOAT, 0, "07801234567.", ""},
 	{"078e0", token.FLOAT, 0, "078e0", ""},
-	{"078", token.INT, 0, "078", "illegal octal number"},
-	{"07800000009", token.INT, 0, "07800000009", "illegal octal number"},
+	{"078", token.INT, 0, "078", "illegal integer number"},
+	{"07800000009", token.INT, 0, "07800000009", "illegal integer number"},
 	{"0x", token.INT, 0, "0x", "illegal hexadecimal number"},
 	{"0X", token.INT, 0, "0X", "illegal hexadecimal number"},
 	{"0Xbeef_", token.INT, 6, "0Xbeef_", "illegal '_' in number"},
 	{"0b", token.INT, 0, "0b", "illegal binary number"},
-	{"0B", token.INT, 0, "0B", "illegal binary number"},
+	{"0o", token.INT, 0, "0o", "illegal octal number"},
 	// {"123456789012345678890_i", IMAG, 21, "123456789012345678890_i", "illegal '_' in number"},
 	{"\"abc\x00def\"", token.STRING, 4, "\"abc\x00def\"", "illegal character NUL"},
 	{"\"abc\x80def\"", token.STRING, 4, "\"abc\x80def\"", "illegal UTF-8 encoding"},
diff --git a/cue/types_test.go b/cue/types_test.go
index 7eeae0b..fca29ae 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -925,7 +925,7 @@
 	}, {
 		config: config,
 		path:   strList("b", "d", "lookup in non-struct"),
-		str:    "not of right kind (int vs struct)",
+		str:    "not of right kind (number vs struct)",
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.str, func(t *testing.T) {
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index c0ea20a..8fba6dd 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -298,20 +298,17 @@
             "." decimals multiplier.
 binary_lit  = "0b" binary_digit { binary_digit } .
 hex_lit     = "0" ( "x" | "X" ) hex_digit { [ "_" ] hex_digit } .
+octal_lit   = "0o" octal_digit { [ "_" ] octal_digit } .
 multiplier  = ( "K" | "M" | "G" | "T" | "P" | "E" | "Y" | "Z" ) [ "i" ]
 ```
 
-<!--
-octal_lit   = "0" octal_digit { [ "_" ] octal_digit } .
-TODO: consider 0o766 notation for octal.
---->
-
 ```
 42
 1.5Gi
-0600
-0xBad_Face
 170_141_183_460_469_231_731_687_303_715_884_105_727
+0xBad_Face
+0o755
+0b0101_0001
 ```
 
 ### Decimal floating-point literals
diff --git a/internal/third_party/yaml/decode.go b/internal/third_party/yaml/decode.go
index 5a970d2..bc42da9 100644
--- a/internal/third_party/yaml/decode.go
+++ b/internal/third_party/yaml/decode.go
@@ -456,7 +456,13 @@
 		return d.ident(n, str)
 
 	case yaml_INT_TAG:
-		return d.makeNum(n, n.value, token.INT)
+		// Convert YAML octal to CUE octal. If YAML accepted an invalid
+		// integer, just convert it as well to ensure CUE will fail.
+		s := n.value
+		if len(s) > 1 && s[0] == '0' && s[1] <= '9' {
+			s = "0o" + s[1:]
+		}
+		return d.makeNum(n, s, token.INT)
 
 	case yaml_FLOAT_TAG:
 		value := n.value
diff --git a/internal/third_party/yaml/decode_test.go b/internal/third_party/yaml/decode_test.go
index be4d034..2e2122d 100644
--- a/internal/third_party/yaml/decode_test.go
+++ b/internal/third_party/yaml/decode_test.go
@@ -122,7 +122,7 @@
 		"decimal: +685_230",
 	}, {
 		"octal: 02472256",
-		"octal: 02472256",
+		"octal: 0o2472256",
 	}, {
 		"hexa: 0x_0A_74_AE",
 		"hexa: 0x_0A_74_AE",