Skip to content

Commit 2dcbbef

Browse files
committed
Give min and max the kind of their untyped arguments, cover the evaluator
max(5, 4.0) has an untyped float type even though the integer wins, so max(5, 4.0) / 2 * 10 is 25 and not 20. A right shift count is bounded by a constant instead of the width of the value, which reaches the same result without measuring it. Tests cover every branch of the evaluator, including the error paths of each operand position, and the package is back to full statement coverage.
1 parent a947fab commit 2dcbbef

3 files changed

Lines changed: 110 additions & 32 deletions

File tree

internal/generator/constexpr.go

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import (
55
"go/ast"
66
"go/constant"
77
"go/token"
8-
"math/big"
98
"strconv"
109
"unicode/utf8"
1110
)
1211

13-
// maxShiftLeft caps a left shift so a malformed source can't ask for an enormous allocation. a right
14-
// shift needs no cap, shifting past the width of a value settles at 0 or -1.
15-
const maxShiftLeft = 512
12+
const (
13+
// maxShiftLeft caps a left shift so a malformed source can't ask for an enormous allocation
14+
maxShiftLeft = 512
15+
// maxShiftRight bounds a right shift count, which only has to reach past the width of the value;
16+
// no constant comes near it, and it keeps the count in range of uint
17+
maxShiftRight = 1 << 20
18+
)
1619

1720
// intTypeInfo describes the width and signedness of a builtin integer type
1821
type intTypeInfo struct {
@@ -357,11 +360,9 @@ func (r *constResolver) evalShift(e *ast.BinaryExpr, x typedValue, iotaVal int64
357360
switch {
358361
case e.Op == token.SHL && (!exact || count > maxShiftLeft):
359362
return typedValue{}, fmt.Errorf("shift count %s is too large", y.ExactString())
360-
case e.Op == token.SHR:
361-
// a shift wider than the value itself keeps its result, clamp it to stay in range of uint
362-
if width := bitLen(xv) + 1; !exact || count > width {
363-
count = width
364-
}
363+
case e.Op == token.SHR && (!exact || count > maxShiftRight):
364+
// shifting further than the value is wide keeps giving the same result
365+
count = maxShiftRight
365366
}
366367
return typedValue{value: constant.Shift(xv, e.Op, uint(count)), typ: x.typ}, nil
367368
}
@@ -431,7 +432,7 @@ func (r *constResolver) evalMinMax(e *ast.CallExpr, name string, iotaVal int64)
431432
}
432433

433434
var best typedValue
434-
typ := ""
435+
typ, anyFloat := "", false
435436
for i, arg := range e.Args {
436437
v, err := r.eval(arg, iotaVal)
437438
if err != nil {
@@ -443,14 +444,19 @@ func (r *constResolver) evalMinMax(e *ast.CallExpr, name string, iotaVal int64)
443444
if typ == "" {
444445
typ = v.typ // a typed argument gives the result its type
445446
}
447+
anyFloat = anyFloat || v.value.Kind() == constant.Float
446448
if i == 0 || constant.Compare(v.value, op, best.value) {
447449
best = v
448450
}
449451
}
450-
if typ == "" {
451-
return best, nil
452+
if typ != "" {
453+
return r.convert(best, typ)
454+
}
455+
if anyFloat {
456+
// an untyped float argument makes the result untyped float, whichever argument won
457+
return typedValue{value: constant.ToFloat(best.value)}, nil
452458
}
453-
return r.convert(best, typ)
459+
return best, nil
454460
}
455461

456462
// roundFloat drops the precision a float type cannot hold, the compiler stores a typed float
@@ -464,18 +470,6 @@ func roundFloat(v constant.Value, typ string) constant.Value {
464470
return constant.MakeFloat64(f)
465471
}
466472

467-
// bitLen is the number of bits an integer value occupies
468-
func bitLen(v constant.Value) uint64 {
469-
i, ok := constant.Val(v).(*big.Int)
470-
if !ok {
471-
return 64 // anything go/constant keeps as an int64
472-
}
473-
if n := i.BitLen(); n > 0 {
474-
return uint64(n)
475-
}
476-
return 0
477-
}
478-
479473
// unparen strips the parentheses around an expression
480474
func unparen(expr ast.Expr) ast.Expr {
481475
for {
@@ -524,13 +518,7 @@ func (r *constResolver) convert(v typedValue, typ string) (typedValue, error) {
524518
// the language allows
525519
func literalValue(lit *ast.BasicLit) (constant.Value, error) {
526520
switch lit.Kind {
527-
case token.INT, token.FLOAT:
528-
v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0)
529-
if v.Kind() == constant.Unknown {
530-
return nil, fmt.Errorf("invalid literal %s", lit.Value)
531-
}
532-
return v, nil
533-
case token.STRING:
521+
case token.INT, token.FLOAT, token.STRING:
534522
v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0)
535523
if v.Kind() == constant.Unknown {
536524
return nil, fmt.Errorf("invalid literal %s", lit.Value)

internal/generator/constexpr_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ func TestConstResolverValues(t *testing.T) {
9898
{"string of a code point", `len(string(0x100))`, "2"},
9999
{"min of a typed argument", "^max(2, uint8(1))", "253"},
100100
{"max of a typed argument", "min(uint8(7), 9) + 1", "8"},
101+
{"max with an untyped float", "max(5, 4.0) / 2 * 10", "25"},
102+
{"min with an untyped float", "min(5, 6.0) / 2 * 10", "25"},
103+
{"max of integers only", "max(5, 4) / 2 * 10", "20"},
104+
{"string of a value out of range", "len(string(-1))", "3"},
105+
{"shift right of a wide value", "1<<70 >> 2000", "0"},
106+
// the compiler rejects a shift count this large outright, the result is still 0
107+
{"shift right past the bound", "1 >> 2000000", "0"},
101108
}
102109

103110
for _, tt := range tests {
@@ -136,6 +143,29 @@ func TestConstResolverErrors(t *testing.T) {
136143
{"comparison operator", "const x = 1 < 2", "unsupported binary operator"},
137144
{"self reference", "const x = x + 1", "refers to itself"},
138145
{"reference cycle", "const (\n\tx = y\n\ty = x\n)", "refers to itself"},
146+
{"failing operand of a negation", "const x = -missing", "unknown constant missing"},
147+
{"complement of a string", `const x = ^"str"`, "not an integer"},
148+
{"failing right operand", "const x = 1 + missing", "unknown constant missing"},
149+
{"operand out of range for the type", "const x = uint8(1) + 300", "overflows uint8"},
150+
{"string added to a number", `const x = 1 + "s"`, "not a number"},
151+
{"number added to a string", `const x = "s" + 1`, "not a number"},
152+
{"fractional remainder", "const x = 1.5 % 2", "not an integer"},
153+
{"remainder by a fraction", "const x = 2 % 1.5", "not an integer"},
154+
{"shift of a string", `const x = "s" << 1`, "not an integer"},
155+
{"failing shift count", "const x = 1 << missing", "unknown constant missing"},
156+
{"fractional shift count", "const x = 1 << 1.5", "not an integer"},
157+
{"conversion with two arguments", "const x = uint8(1, 2)", "unsupported call expression"},
158+
{"min without arguments", "const x = min()", "at least one argument"},
159+
{"failing min argument", "const x = min(missing, 1)", "unknown constant missing"},
160+
{"min of a string", `const x = min("a", 1)`, "not a number"},
161+
{"failing len argument", "const x = len(missing)", "unknown constant missing"},
162+
{"failing conversion argument", "const x = uint8(missing)", "unknown constant missing"},
163+
{"fractional conversion", "const x = uint8(1.5)", "not an integer"},
164+
{"float conversion of a string", `const x = float64("s")`, "not a number"},
165+
{"string conversion of a fraction", "const (\n\tx = str(1.5)\n)\ntype str string", "not a string"},
166+
{"call of an expression", "const x = (1 + 1)(2)", "unsupported call expression"},
167+
{"failing left operand of a typed sum", "const x = 300 + uint8(1)", "overflows uint8"},
168+
{"alias cycle", "const x = ^a(0)\ntype a = b\ntype b = a", "unsupported call to a"},
139169
}
140170

141171
for _, tt := range tests {
@@ -322,6 +352,27 @@ func TestCheckIntRange(t *testing.T) {
322352
}
323353
}
324354

355+
func TestConstResolverDeclarations(t *testing.T) {
356+
// a name declared twice keeps the first declaration, which is what a compiling package has
357+
v, err := resolveSrc(t, "package p\nconst x = 1\nconst x = 2\n", "x")
358+
require.NoError(t, err)
359+
assert.Equal(t, "1", v.ExactString())
360+
361+
// specs that are not value or type declarations are skipped
362+
r := newConstResolver()
363+
r.addFile(&ast.File{
364+
Name: &ast.Ident{Name: "p"},
365+
Decls: []ast.Decl{
366+
&ast.GenDecl{Tok: token.TYPE, Specs: []ast.Spec{&ast.ImportSpec{}}},
367+
&ast.GenDecl{Tok: token.CONST, Specs: []ast.Spec{&ast.ImportSpec{}}},
368+
&ast.GenDecl{Tok: token.IMPORT, Specs: []ast.Spec{&ast.ImportSpec{}}},
369+
&ast.FuncDecl{Name: &ast.Ident{Name: "f"}},
370+
},
371+
})
372+
assert.Empty(t, r.decls)
373+
assert.Empty(t, r.types)
374+
}
375+
325376
func TestConstResolverUnsupportedNodes(t *testing.T) {
326377
r := newConstResolver()
327378

@@ -345,6 +396,18 @@ func TestConstResolverUnsupportedNodes(t *testing.T) {
345396
require.Error(t, err)
346397
assert.Contains(t, err.Error(), "not a number")
347398

399+
for _, lit := range []*ast.BasicLit{
400+
{Kind: token.INT, Value: "12abc"},
401+
{Kind: token.FLOAT, Value: "1.2.3"},
402+
{Kind: token.CHAR, Value: "'"},
403+
{Kind: token.CHAR, Value: "abc"},
404+
{Kind: token.CHAR, Value: `'\q'`},
405+
} {
406+
_, err = literalValue(lit)
407+
require.Error(t, err, lit.Value)
408+
assert.Contains(t, err.Error(), "invalid literal", lit.Value)
409+
}
410+
348411
_, err = r.resolve("nothing")
349412
require.Error(t, err)
350413
assert.Contains(t, err.Error(), "unknown constant nothing")
@@ -472,6 +535,32 @@ const (
472535
assert.Contains(t, err.Error(), "const codeB: value -1 is negative but the type is uint8")
473536
}
474537

538+
func TestParseUntypedValueOutOfRange(t *testing.T) {
539+
// a constant without a type of its own still has to fit the underlying type of the enum
540+
src := `package test
541+
type small int8
542+
const (
543+
smallA small = 100
544+
smallB = 200
545+
)
546+
`
547+
_, err := parseSrc(t, "small", src)
548+
require.Error(t, err)
549+
assert.Contains(t, err.Error(), "const smallB: value 200 overflows int8")
550+
}
551+
552+
func TestParseConstWithoutValue(t *testing.T) {
553+
src := `package test
554+
type status int
555+
const (
556+
statusA
557+
)
558+
`
559+
_, err := parseSrc(t, "status", src)
560+
require.Error(t, err)
561+
assert.Contains(t, err.Error(), "no value for const statusA")
562+
}
563+
475564
func TestParseValueOutOfRange(t *testing.T) {
476565
src := `package test
477566
type small int8

internal/generator/generator_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,7 @@ func TestParseAliasComment(t *testing.T) {
14491449
{"multiple aliases", "// enum:alias=rw,read-write", []string{"rw", "read-write"}},
14501450
{"with whitespace", "// enum:alias= rw , read-write ", []string{"rw", "read-write"}},
14511451
{"empty value", "// enum:alias=", nil},
1452+
{"only separators", "// enum:alias=,,", nil},
14521453
{"empty between commas", "// enum:alias=a,,b", []string{"a", "b"}},
14531454
{"no alias directive", "// some comment", nil},
14541455
{"nil comment", "", nil},

0 commit comments

Comments
 (0)