Skip to content

Commit f8ee956

Browse files
committed
internal/encoding/yaml/goccy: resolve scalar numbers by syntax rules
numberKind mirrored the yaml.v3 resolver through Go's strconv functions, inheriting artifacts that belong to neither YAML nor CUE: integers were bounded to 64 bits, so larger decimal integers decoded as the float form `number & N` and larger hexadecimals fell back to strings, even though YAML and CUE integers have no size limit, and values shaped like broken YAML 1.1 octals, such as `0778`, resolved as floats and needed a carve-out to reach their string interpretation. Resolve numbers by declared syntax rules instead: the YAML 1.2 core schema forms extended with the YAML 1.1 forms CUE keeps supporting, namely leading-zero octals, binary integers, a sign before any base, and underscore separators. Integers of any size in any base now decode as CUE integers, the `0778` carve-out disappears since a decimal integer must not start with a zero digit, and `number & N` remains only for scalars tagged `!!float` whose value is written as an integer. The rxAnyOctalYaml11 pattern moves to the goccy encoder added by a follow-up change, whose quoting still relies on it. Converting YAML 1.1 octals to CUE form is now also limited to integer shapes and understands signs. It previously fired on any value with a leading zero digit, so valid YAML floats such as `01289.5` were mangled into `0o1289.5` and failed to decode, and it ignored signed octals such as `-0123`, which then failed to decode as invalid CUE. Both bugs are shared with the yaml.v3 based decoder, where they remain; the yamlgoccy experiment covers the transition. Signed-off-by: Matthew Sackman <matthew@cue.works> Assisted-by: Claude Code (claude-fable-5) Change-Id: Ie93e11aa537efceebb91ade0c699acf96d93f8a6 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1244090 TryBot-Result: CUEcueckoo <cueckoo@cuelang.org> Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com> Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
1 parent cf81409 commit f8ee956

3 files changed

Lines changed: 66 additions & 56 deletions

File tree

cmd/cue/cmd/testdata/script/import_yaml_numbers.txtar

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ float: !!float 123
3939
memory: "1Gi"
4040
cpu: "100m"
4141
frac: "1.5Gi"
42-
huge: number & 18446744073709551616
42+
huge: 18446744073709551616
4343
exp: 123456e1
4444
float: number & 123
4545
-- expect-import-old-stderr --

internal/encoding/yaml/goccy/decode.go

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,16 +1454,24 @@ func infString(n *yast.InfinityNode) string {
14541454
return "+Inf"
14551455
}
14561456

1457-
// yamlStyleFloat matches the plain scalars that resolve as floats when
1458-
// they do not parse as integers; see [numberKind].
1459-
var yamlStyleFloat = sync.OnceValue(func() *regexp.Regexp {
1460-
return regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`)
1457+
// rxYamlInt matches the plain scalars this package resolves as
1458+
// integers: the YAML 1.2 core schema forms extended with the YAML 1.1
1459+
// forms CUE keeps supporting, namely leading-zero octals, binary
1460+
// integers, and a sign before any base. A decimal integer must not
1461+
// start with a zero digit, so that broken octals such as `0778`
1462+
// resolve as strings. Underscore separators are stripped before
1463+
// matching; see [numberKind].
1464+
var rxYamlInt = sync.OnceValue(func() *regexp.Regexp {
1465+
return regexp.MustCompile(`^[-+]?(0|[1-9][0-9]*|0b[01]+|0o?[0-7]+|0x[0-9a-fA-F]+)$`)
14611466
})
14621467

1463-
// rxAnyOctalYaml11 uses the implicit tag resolution regular expression for base-8 integers
1464-
// from YAML's 1.1 spec, but including the 8 and 9 digits which aren't valid for octal integers.
1465-
var rxAnyOctalYaml11 = sync.OnceValue(func() *regexp.Regexp {
1466-
return regexp.MustCompile(`^[-+]?0[0-9_]+$`)
1468+
// rxYamlFloat matches the plain scalars this package resolves as
1469+
// floats: decimal digits with a dot, an exponent, or both. Requiring
1470+
// the dot or exponent keeps decimal integers, whatever their leading
1471+
// digit, out of the float form. Underscore separators are stripped
1472+
// before matching; see [numberKind].
1473+
var rxYamlFloat = sync.OnceValue(func() *regexp.Regexp {
1474+
return regexp.MustCompile(`^[-+]?((\.[0-9]+|[0-9]+\.[0-9]*)([eE][-+]?[0-9]+)?|[0-9]+[eE][-+]?[0-9]+)$`)
14671475
})
14681476

14691477
// specialFloats maps the plain scalar spellings of infinities and NaN
@@ -1477,36 +1485,24 @@ var specialFloats = map[string]string{
14771485

14781486
// numberKind reports whether this package resolves the plain scalar s
14791487
// as a number, returning token.INT, token.FLOAT, or token.ILLEGAL when
1480-
// s is not a number. Infinities and NaN are handled separately via
1481-
// [specialFloats]. Note that we cannot use CUE's own number syntax to
1482-
// decide, as it is a superset of YAML's: for example, `1Gi` is a valid
1483-
// CUE literal but a string in YAML.
1488+
// s is not a number. The accepted forms are those of [rxYamlInt] and
1489+
// [rxYamlFloat], with no limit on the size of integers. Infinities and
1490+
// NaN are handled separately via [specialFloats]. Note that we cannot
1491+
// use CUE's own number syntax to decide, as it is a superset of
1492+
// YAML's: for example, `1Gi` is a valid CUE literal but a string in
1493+
// YAML.
14841494
func numberKind(s string) token.Token {
1485-
if s == "" {
1495+
if s == "" || s[0] == '_' {
14861496
return token.ILLEGAL
14871497
}
1488-
switch c := s[0]; {
1489-
case c == '.':
1490-
// A float such as `.5e3`.
1491-
if _, err := strconv.ParseFloat(s, 64); err == nil {
1492-
return token.FLOAT
1493-
}
1494-
case c == '+' || c == '-' || (c >= '0' && c <= '9'):
1495-
// Strip underscores, which YAML 1.1 allows as separators.
1496-
plain := strings.ReplaceAll(s, "_", "")
1497-
if _, err := strconv.ParseInt(plain, 0, 64); err == nil {
1498-
return token.INT
1499-
}
1500-
if _, err := strconv.ParseUint(plain, 0, 64); err == nil {
1501-
return token.INT
1502-
}
1503-
// A float, such as an integer beyond 64 bits or an exponent
1504-
// without a decimal point like `123456e1`.
1505-
if yamlStyleFloat().MatchString(plain) {
1506-
if _, err := strconv.ParseFloat(plain, 64); err == nil {
1507-
return token.FLOAT
1508-
}
1509-
}
1498+
// Strip underscores, which YAML 1.1 allows as separators within
1499+
// numbers.
1500+
plain := strings.ReplaceAll(s, "_", "")
1501+
switch {
1502+
case rxYamlInt().MatchString(plain):
1503+
return token.INT
1504+
case rxYamlFloat().MatchString(plain):
1505+
return token.FLOAT
15101506
}
15111507
return token.ILLEGAL
15121508
}
@@ -1529,24 +1525,30 @@ func (d *decoder) scalarString(n *yast.StringNode) (ast.Expr, error) {
15291525
case token.INT:
15301526
return d.intExpr(pos, value)
15311527
case token.FLOAT:
1532-
// Values which look like YAML 1.1 octal literals but aren't
1533-
// valid octal integers, such as `0778`, are interpreted as
1534-
// strings instead, as most YAML decoders do.
1535-
if !rxAnyOctalYaml11().MatchString(value) {
1536-
return d.floatExpr(pos, value, true)
1537-
}
1528+
return d.floatExpr(pos, value, true)
15381529
}
15391530
}
15401531
return d.quotedString(pos, value), nil
15411532
}
15421533

1543-
// yaml11OctalToCUE converts a YAML 1.1 octal literal like 0777 to CUE
1544-
// form. Other values are returned unchanged.
1534+
// yaml11OctalToCUE converts a YAML 1.1 octal literal like 0777 or
1535+
// -0777 to CUE form. Other values, such as floats with a leading zero
1536+
// digit like 01289.5, are returned unchanged.
15451537
func yaml11OctalToCUE(value string) string {
1546-
if len(value) > 1 && value[0] == '0' && value[1] >= '0' && value[1] <= '9' {
1547-
return "0o" + value[1:]
1538+
sign, digits := "", value
1539+
if len(digits) > 0 && (digits[0] == '+' || digits[0] == '-') {
1540+
sign, digits = digits[:1], digits[1:]
1541+
}
1542+
rest, ok := strings.CutPrefix(digits, "0")
1543+
if !ok || rest == "" {
1544+
return value
1545+
}
1546+
for _, c := range rest {
1547+
if (c < '0' || c > '7') && c != '_' {
1548+
return value
1549+
}
15481550
}
1549-
return value
1551+
return sign + "0o" + rest
15501552
}
15511553

15521554
func (d *decoder) integer(n *yast.IntegerNode) (ast.Expr, error) {

internal/encoding/yaml/goccy/decode_test.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,15 @@ var unmarshalTests = []struct {
152152
}, {
153153
"octal_yaml11: 02472256",
154154
"octal_yaml11: 0o2472256",
155+
}, {
156+
"octal_yaml11_neg: -02472256",
157+
"octal_yaml11_neg: -0o2472256",
155158
}, {
156159
"octal_yaml12: 0o2472256",
157160
"octal_yaml12: 0o2472256",
161+
}, {
162+
"float_leading_zero: 01289.5",
163+
"float_leading_zero: 01289.5",
158164
}, {
159165
"not_octal_yaml11: 0123456789",
160166
`not_octal_yaml11: "0123456789"`,
@@ -400,7 +406,7 @@ null: 1
400406
},
401407
{
402408
"float32_maxuint64+1: 18446744073709551616",
403-
`"float32_maxuint64+1": number & 18446744073709551616`,
409+
`"float32_maxuint64+1": 18446744073709551616`,
404410
},
405411

406412
// float64
@@ -416,22 +422,24 @@ null: 1
416422
"float64_maxuint64: 18446744073709551615",
417423
"float64_maxuint64: 18446744073709551615",
418424
},
419-
// TODO(mvdan): numberKind uses strconv APIs like ParseUint to decide
420-
// whether a scalar is a YAML integer or a float.
421-
// Integers in CUE aren't limited to 64 bits, so we should arguably not decode
422-
// large integers that don't fit in 64 bits as floats via `number &`.
425+
// Integers are unbounded, like CUE's, with no 64-bit cliff in any
426+
// base.
423427
{
424428
"float64_maxuint64+1: 18446744073709551616",
425-
`"float64_maxuint64+1": number & 18446744073709551616`,
429+
`"float64_maxuint64+1": 18446744073709551616`,
426430
},
427431
{
428432
"v: -9223372036854775809",
429-
"v: number & -9223372036854775809",
433+
"v: -9223372036854775809",
434+
},
435+
{
436+
"v: 36_893_488_147_419_103_232",
437+
"v: 36_893_488_147_419_103_232",
430438
},
431439

432440
// Scalars that are valid CUE numbers but not YAML numbers must stay
433441
// strings, such as Kubernetes resource quantities with multiplier
434-
// suffixes or hexadecimals beyond 64 bits.
442+
// suffixes.
435443
{
436444
"memory: 1Gi",
437445
`memory: "1Gi"`,
@@ -450,7 +458,7 @@ null: 1
450458
},
451459
{
452460
"v: 0xFFFFFFFFFFFFFFFFF",
453-
`v: "0xFFFFFFFFFFFFFFFFF"`,
461+
"v: 0xFFFFFFFFFFFFFFFFF",
454462
},
455463

456464
// Overflow cases.

0 commit comments

Comments
 (0)