Skip to content

Evaluate enum constants with go/constant - #24

Merged
umputun merged 4 commits into
masterfrom
fix/const-expr-eval
Aug 19, 2026
Merged

Evaluate enum constants with go/constant#24
umputun merged 4 commits into
masterfrom
fix/const-expr-eval

Conversation

@paskal

@paskal paskal commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Previously the generator recognised only iota, a plain literal, and a binary expression combining iota with one of +, -, * or /. Every other constant expression silently became zero, because the evaluator returned an error that was then discarded in favour of a zero value. That covered shifts, bitwise operators, parentheses, references to other constants, and every literal that is not plain decimal, since the literal converter scanned with %d and stopped at the first character it did not recognise. An enum written as

const (
    permRead perm = 1 << iota
    permWrite
    permExecute
)

generated value: 0 for all three constants, and 0x10, 0b1010 and 1_000 became zero in the same way. With -getter the duplicate zeroes failed generation instead.

On one source file, the same constants before and after:

constant expression before after
permRead, permWrite, permExec 1 << iota 0, 0, 0 1, 2, 4
permAll ^perm(0) 0 255
permBase base, declared as 0x10 0 16
permCalc perm(base/2 + len("ab")) 0 10
permNeg 1_0 - 2 -1 8

The -1 in the last row was written into a uint8 field, so that file did not compile at all. Every value in the last column is the one the compiler computes for the same constant, checked by generating the enum and comparing Index() against the original constant in a test.

After this change constant expressions are evaluated with go/constant. The whole integer operator set is supported, together with literals in any base, conversions, len, min, max, and references to other constants declared in the package. The type a constant carries is followed through the expression, since it decides several results: ^perm(0) complements within the width of perm, a typed integer divides as an integer, and a constant declared float32 holds its value at the width of that type. Values are kept exact rather than narrowed to int, so a uint64 enum above MaxInt64 keeps its value. A constant declared without an expression repeats the expression of the preceding one, which is the rule the language itself applies, instead of replaying a recorded operation. Each of these cases was checked against the value the compiler computes for the same constant, in a differential comparison over 38 expressions and by generating enums and comparing Index() against the original constant.

Values that cannot be evaluated, and values that do not fit the underlying type of the enum, are now reported as errors rather than becoming zero. Two things are deliberately left out and named in the error: a constant of another package, which would need the type checker and a resolvable import graph, and the constant built-ins other than len, which do not appear in enum declarations. The complement of int, uint and uintptr is refused as well, since its value depends on the width of the target architecture while the generated file holds one number.

What changes for existing users:

  • An enum built on an unsupported expression generated wrong values before and generates correct ones now. Index() and Get...ByID change accordingly, so a project that persisted those integers needs to consider a migration.
  • Generation now fails, instead of writing a file, when a constant cannot be evaluated, for instance when it refers to a constant of another package, and when its value overflows the underlying type of the enum.
  • The generated API is unchanged.

paskal added 4 commits August 19, 2026 02:02
Previously the generator understood only iota, plain literals and a binary
expression with one of + - * / around iota. Anything else silently produced 0:
shifts, bitwise operators, parentheses, references to other constants, and every
literal that is not plain decimal, since the literal converter scanned with %d
and stopped at the first character it did not understand.

After this change constant expressions are evaluated with go/constant, which
covers the whole integer operator set and keeps values exact, so a uint64 enum
past MaxInt64 keeps its value. A spec without an expression now repeats the
expression of the previous spec, the rule the language itself uses, instead of
replaying a recorded operation. Values that cannot be evaluated, or that do not
fit the underlying type, are reported as errors instead of becoming 0.
The complement of an unsigned constant depends on the width of its type, so
^perm(0) of a uint8 based enum is 255 and not -1. Values now carry the builtin
type they were converted to, and a conversion reports the values that type
cannot hold, matching what the compiler accepts.

Constants declared inside a function are no longer collected: they are not in
scope for the enum values and a local name would shadow the package level one.
Arithmetic on constants with a fractional literal is evaluated exactly, as the
language does, so 1.5 * 2 is 3 rather than an error, and len of a string
literal is supported.
A typed operand decides the type of an operation, so x / 2.0 is an integer
division when x is a typed integer, and a value reached through a float keeps
its exact result. A typed float holds its value at the width of its type, both
after a conversion and after each operation. Conversions to float and string
are evaluated rather than refused, len works on any string constant, min and
max are evaluated, and a type declared by the package is followed before a
builtin of the same name, through an alias chain of any length.

A right shift is no longer capped: shifting past the width of a value settles
at 0, or -1 when it is negative, which is what the compiler computes. The cap
on a left shift stays, it bounds the size of the value being built.

The complement of uint and uintptr is refused, it is a different value on a 32
and a 64 bit target while the generated file holds one number.
The type a constant is declared with shapes its value: a float one holds it at
the width of that type, so a constant declared float32 rounds the way the
compiler rounds it. min and max return their result in the type of a typed
argument, and the underlying type of the enum is read through parentheses, so
type code (uint8) is recognised as unsigned and rejects a negative value.
@paskal
paskal requested a review from umputun as a code owner August 19, 2026 01:46
@umputun
umputun merged commit f5a4862 into master Aug 19, 2026
2 of 4 checks passed
@umputun
umputun deleted the fix/const-expr-eval branch August 19, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants