perf(number): skip the rebuild for tokens without underscores

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-17 23:12:00 +02:00
parent f1a757ec5c
commit 53102d70e6
2 changed files with 37 additions and 9 deletions
+4
View File
@@ -47,6 +47,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the input is validated UTF-8 up front, so the conversion pass and its four
bytes per rune were pure overhead. Parsing a large array-of-tables document
runs about a fifth faster and allocates about half the memory.
- Numeric tokens without underscores skip the normalising rebuild: digits are
validated in place in `joinDigits`, and a float whose token is already
clean goes to `strconv.ParseFloat` directly. One allocation per integer
atom and two per float atom disappear.
### Fixed
+33 -9
View File
@@ -94,28 +94,43 @@ func decodeFloat(tok string) (any, error) {
if err := checkNoLeadingZero(ip); err != nil {
return nil, err
}
build := sign + ip
fp := ""
if hasDot {
fp, err := joinDigits(frac, isDecDigit)
if err != nil {
if fp, err = joinDigits(frac, isDecDigit); err != nil {
return nil, err
}
build += "." + fp
}
// The ABNF requires at least one digit after the exponent marker, so a
// trailing e or E is an error even though strconv would accept it. The
// digits are a zero-prefixable integer, so leading zeros are fine here
// (the corpus holds valid cases such as 1e06 and 0e00).
esign, ed := "", ""
if hasExp {
esign, edigits := splitSign(exp)
ed, err := joinDigits(edigits, isDecDigit)
if err != nil {
var digits string
esign, digits = splitSign(exp)
if ed, err = joinDigits(digits, isDecDigit); err != nil {
return nil, err
}
build += "e" + esign + ed
}
// The checks above validated the token's shape, and every character a
// valid token may carry is one strconv.ParseFloat accepts in place, so
// only a token with underscores needs the stripped rebuild.
if !strings.ContainsRune(tok, '_') {
f, err := strconv.ParseFloat(tok, 64)
if err != nil {
return nil, fmt.Errorf("invalid float %q", tok)
}
return f, nil
}
build := sign + ip
if hasDot {
build += "." + fp
}
if hasExp {
build += "e" + esign + ed
}
f, err := strconv.ParseFloat(build, 64)
if err != nil {
return nil, fmt.Errorf("invalid float %q", tok)
@@ -125,11 +140,20 @@ func decodeFloat(tok string) (any, error) {
// joinDigits validates that every rune is a digit (per isDigit) and that each
// underscore sits between two digits, returning the digits with underscores
// removed.
// removed. A token without underscores, the common case, is validated in
// place and returned without a copy.
func joinDigits(s string, isDigit func(byte) bool) (string, error) {
if s == "" {
return "", fmt.Errorf("number is missing digits")
}
if !strings.ContainsRune(s, '_') {
for i := range len(s) {
if !isDigit(s[i]) {
return "", fmt.Errorf("invalid character %q in number", string(s[i]))
}
}
return s, nil
}
var b strings.Builder
for i := range len(s) {
c := s[i]