aboutsummaryrefslogtreecommitdiffstats
path: root/ccl.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-10-30 18:29:34 -0700
committerRose Hogenson <rosehogenson@posteo.net>2025-10-30 18:34:25 -0700
commit74235daa0ae145c96b63c980e98c9c169f5c0007 (patch)
treec7b3a29cda03cee565e8cf223dac00c8e08af91c /ccl.go
parent3888d15001e4f6ee8319874184b78c072fca577e (diff)
downloadccl-74235daa0ae145c96b63c980e98c9c169f5c0007.tar.zst
Don't allocate an intermediate map[string]any
Not too bad goos: linux goarch: amd64 pkg: roseh.moe/pkg/ccl cpu: AMD Ryzen 9 5900X 12-Core Processor │ baseline.txt │ mapn't.txt │ │ sec/op │ sec/op vs base │ Parse-24 8.880µ ± 0% 7.885µ ± 0% -11.21% (p=0.000 n=20)
Diffstat (limited to 'ccl.go')
-rw-r--r--ccl.go359
1 files changed, 159 insertions, 200 deletions
diff --git a/ccl.go b/ccl.go
index fe67a72..3a444c1 100644
--- a/ccl.go
+++ b/ccl.go
@@ -243,11 +243,12 @@ func fieldMap(out map[structField]int, types map[reflect.Type]bool, s reflect.Ty
}
type parser struct {
- nextTok func() (token, error, bool)
- tok []byte
- err error
- data []byte
- i int
+ nextTok func() (token, error, bool)
+ tok []byte
+ err error
+ data []byte
+ i int
+ fieldMap map[structField]int
}
func (p *parser) error(reason string, args ...any) error {
@@ -284,6 +285,9 @@ func (p *parser) next() ([]byte, error) {
}
func checkNum(b []byte) bool {
+ if b[0] == '-' || b[0] == '+' {
+ b = b[1:]
+ }
if bytes.Equal(b, []byte("0")) {
return true
}
@@ -323,7 +327,7 @@ type integer struct {
sgn int8
}
-func (p *parser) parseNum(numBytes []byte) (any, error) {
+func (p *parser) parseInt(numBytes []byte) (integer, error) {
n := numBytes
var sgn int8 = 1
switch numBytes[0] {
@@ -336,25 +340,29 @@ func (p *parser) parseNum(numBytes []byte) (any, error) {
if len(n) > 2 && n[0] == '0' && (n[1] == 'x' || n[1] == 'X') {
n, err := strconv.ParseUint(string(n[2:]), 16, 64)
if err != nil {
- return nil, p.error("invalid hex number: %s", err)
+ return integer{}, p.error("invalid hex number: %s", err)
}
- return &integer{n, sgn}, nil
- }
- if !checkNum(n) {
- return nil, p.error("invalid number")
+ return integer{n, sgn}, nil
}
- if bytes.ContainsAny(n, ".eE") {
- n, err := strconv.ParseFloat(string(numBytes), 64)
- if err != nil {
- return nil, p.error("invalid number (unreachable)")
- }
- return n, nil
+ if !checkNum(numBytes) {
+ return integer{}, p.error("invalid number")
}
un, err := strconv.ParseUint(string(n), 10, 64)
if err != nil {
- return nil, p.error("invalid number (unreachable)")
+ return integer{}, p.error("invalid number (unreachable)")
}
- return &integer{un, sgn}, nil
+ return integer{un, sgn}, nil
+}
+
+func (p *parser) parseFloat(nBytes []byte) (float64, error) {
+ if !checkNum(nBytes) {
+ return 0, p.error("invalid number")
+ }
+ n, err := strconv.ParseFloat(string(nBytes), 64)
+ if err != nil {
+ return 0, p.error("invalid number (unreachable)")
+ }
+ return n, nil
}
func (p *parser) unescape(rawStr []byte) ([]byte, error) {
@@ -460,134 +468,187 @@ func (p *parser) parseString(tok []byte) (string, error) {
}
}
-func (p *parser) parseMessage() (map[string]any, error) {
- m := make(map[string]any)
+func (p *parser) parseMessage(out reflect.Value, field []byte) error {
+ out = setPtr(out)
+ if out.Kind() != reflect.Struct {
+ return p.error("field %q should be a struct", field)
+ }
+ seen := make(map[string]bool)
for {
tok, err := p.next()
if err != nil || tok[0] == '}' {
- return m, err
+ return err
}
- if err := p.parseFieldVal(m, tok); err != nil {
- return nil, err
+ if err := p.parseFieldVal(out, seen, tok); err != nil {
+ return err
}
}
}
-func (p *parser) parseVal(tok []byte) (any, error) {
- switch tok[0] {
- case '{':
- m, err := p.parseMessage()
- if err != nil {
- return nil, err
+func (p *parser) parsePossiblyRepeatedVal(fieldVal reflect.Value, parsedFields map[string]bool, tok, field []byte) error {
+ if fieldVal.Kind() == reflect.Slice && fieldVal.Type() != reflect.TypeFor[[]byte]() {
+ if tok[0] == '[' {
+ return p.parseList(fieldVal, field)
}
- return m, nil
+ fieldVal.Set(reflect.Append(fieldVal, reflect.Zero(fieldVal.Type().Elem())))
+ return p.parseVal(fieldVal.Index(fieldVal.Len()-1), tok, field)
+ }
+ parsedFields[string(field)] = true
+ return p.parseVal(fieldVal, tok, field)
+}
+
+func (p *parser) parseVal(fieldVal reflect.Value, tok, field []byte) error {
+ switch tok[0] {
case '[':
- return p.parseList()
+ return p.error("invalid repeated value")
+ case '{':
+ return p.parseMessage(fieldVal, field)
case '\'', '"':
s, err := p.parseString(tok)
if err != nil {
- return nil, err
+ return err
}
- return s, nil
- default:
- switch string(tok) {
- case "true", "yes", "on":
- return true, nil
- case "false", "no", "off":
- return false, nil
- default:
- n, err := p.parseNum(tok)
+ if _, ok := fieldVal.Interface().(encoding.TextUnmarshaler); ok {
+ if fieldVal.Kind() == reflect.Pointer && fieldVal.IsNil() {
+ fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
+ }
+ return fieldVal.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s))
+ }
+ if unmarshaler, ok := fieldVal.Addr().Interface().(encoding.TextUnmarshaler); ok {
+ return unmarshaler.UnmarshalText([]byte(s))
+ }
+ fieldVal := setPtr(fieldVal)
+ switch {
+ case fieldVal.Kind() == reflect.String:
+ fieldVal.SetString(s)
+ case fieldVal.Type() == reflect.TypeFor[[]byte]():
+ b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
- return nil, err
+ return fmt.Errorf("field %q: bad base64", field)
}
- return n, nil
+ fieldVal.Set(reflect.ValueOf(b))
+ default:
+ return p.error("field %q should have type string (got %s)", field, fieldVal.Type())
}
+ return nil
+ }
+ switch string(tok) {
+ case "true", "yes", "on":
+ return p.unpackBool(fieldVal, true, field)
+ case "false", "no", "off":
+ return p.unpackBool(fieldVal, false, field)
+ }
+ if bytes.ContainsAny(tok, ".eE") {
+ n, err := p.parseFloat(tok)
+ if err != nil {
+ return err
+ }
+ fieldVal := setPtr(fieldVal)
+ switch fieldVal.Kind() {
+ case reflect.Float32, reflect.Float64:
+ fieldVal.SetFloat(float64(n))
+ default:
+ return p.error("field %q should have type float64 or float32", field)
+ }
+ return nil
+ }
+ n, err := p.parseInt(tok)
+ if err != nil {
+ return err
+ }
+ fieldVal = setPtr(fieldVal)
+ switch fieldVal.Kind() {
+ case reflect.Float32, reflect.Float64:
+ fieldVal.SetFloat(float64(n.sgn) * float64(n.n))
+ return nil
+ }
+ min, max, ok := intLimits(fieldVal.Kind())
+ if !ok {
+ return p.error("field %q should have type int", field)
+ }
+ if n.sgn < 0 && n.n > min || n.sgn > 0 && n.n > max {
+ return p.error("number %d is out of range for %s", n, fieldVal.Kind())
+ }
+ if min == 0 { // unsigned
+ fieldVal.SetUint(n.n)
+ } else {
+ fieldVal.SetInt(int64(n.sgn) * int64(n.n))
}
+ return nil
}
-func (p *parser) parseList() ([]any, error) {
- var l []any
+func (p *parser) parseList(fieldVal reflect.Value, field []byte) error {
+ if fieldVal.IsNil() {
+ fieldVal.Set(reflect.MakeSlice(fieldVal.Type(), 0, 0))
+ }
for i := 0; ; i++ {
tok, err := p.next()
if err != nil || tok[0] == ']' {
- return l, err
+ return err
}
if i > 0 {
if tok[0] != ',' {
- return nil, p.error("expecting comma")
+ return p.error("expecting comma")
}
tok, err = p.next()
if err != nil || tok[0] == ']' { // allow trailing comma
- return l, err
+ return err
}
}
- vs, err := p.parseVal(tok)
- if err != nil {
- return nil, err
+ fieldVal.Set(reflect.Append(fieldVal, reflect.Zero(fieldVal.Type().Elem())))
+ if err := p.parseVal(fieldVal.Index(fieldVal.Len()-1), tok, field); err != nil {
+ return err
}
- l = append(l, vs)
}
}
-func appendAny(prevVal any, newVal any) any {
- if prevVal == nil {
- return newVal
- }
- var l []any
- if ll, ok := prevVal.([]any); ok {
- l = ll
- } else {
- l = []any{prevVal}
- }
- if ll, ok := newVal.([]any); ok {
- return append(l, ll...)
- }
- return append(l, newVal)
-}
-
-func (p *parser) parseFieldVal(m map[string]any, field []byte) error {
+func (p *parser) parseFieldVal(out reflect.Value, parsedFields map[string]bool, field []byte) error {
if b := field[0]; !(b == '_' || 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') {
return p.error("expecting field")
}
+ if parsedFields[string(field)] {
+ return p.error("duplicate field %q but type is not repeated", field)
+ }
+ fieldIdx, ok := p.fieldMap[structField{out.Type(), string(field)}]
+ if !ok {
+ return fmt.Errorf("no field named %q", field)
+ }
+ fieldVal := out.Field(fieldIdx)
tok, err := p.next()
if err != nil {
return err
}
switch tok[0] {
case '{':
- vs, err := p.parseVal(tok)
- if err != nil {
+ if err := p.parsePossiblyRepeatedVal(fieldVal, parsedFields, tok, field); err != nil {
return err
}
- m[string(field)] = appendAny(m[string(field)], vs)
case ':':
tok, err := p.next()
if err != nil {
return err
}
- vs, err := p.parseVal(tok)
- if err != nil {
+ if err := p.parsePossiblyRepeatedVal(fieldVal, parsedFields, tok, field); err != nil {
return err
}
- m[string(field)] = appendAny(m[string(field)], vs)
default:
return p.error("expecting colon")
}
return nil
}
-func (p *parser) parse() (map[string]any, error) {
- m := make(map[string]any)
+func (p *parser) parse(out reflect.Value) error {
+ seen := make(map[string]bool)
for {
tok, err := p.next()
if err != nil {
if err == errEOF {
- return m, nil
+ return nil
}
- return nil, err
+ return err
}
- if err := p.parseFieldVal(m, tok); err != nil {
- return nil, err
+ if err := p.parseFieldVal(out, seen, tok); err != nil {
+ return err
}
}
}
@@ -629,123 +690,25 @@ func intLimits(kind reflect.Kind) (min, max uint64, ok bool) {
}
}
-func unpackVal(fieldVal reflect.Value, fieldMap map[structField]int, val any, field string) error {
- switch val := val.(type) {
- case bool:
- fieldVal := setPtr(fieldVal)
- switch fieldVal.Kind() {
- case reflect.Bool:
- fieldVal.SetBool(val)
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- if val {
- fieldVal.SetInt(1)
- } else {
- fieldVal.SetInt(0)
- }
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- if val {
- fieldVal.SetUint(1)
- } else {
- fieldVal.SetUint(0)
- }
- default:
- return fmt.Errorf("field %q should have type bool", field)
- }
- case *integer:
- fieldVal := setPtr(fieldVal)
- switch fieldVal.Kind() {
- case reflect.Float32, reflect.Float64:
- fieldVal.SetFloat(float64(val.sgn) * float64(val.n))
- return nil
- }
- min, max, ok := intLimits(fieldVal.Kind())
- if !ok {
- return fmt.Errorf("field %q should have type int", field)
- }
- if val.sgn < 0 && val.n > min || val.sgn > 0 && val.n > max {
- return fmt.Errorf("number %d is out of range for %s", val, fieldVal.Kind())
- }
- if min == 0 { // unsigned
- fieldVal.SetUint(val.n)
+func (p *parser) unpackBool(fieldVal reflect.Value, b bool, field []byte) error {
+ fieldVal = setPtr(fieldVal)
+ switch fieldVal.Kind() {
+ case reflect.Bool:
+ fieldVal.SetBool(b)
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ if b {
+ fieldVal.SetInt(1)
} else {
- fieldVal.SetInt(int64(val.sgn) * int64(val.n))
+ fieldVal.SetInt(0)
}
- case float64:
- fieldVal := setPtr(fieldVal)
- switch fieldVal.Kind() {
- case reflect.Float32, reflect.Float64:
- fieldVal.SetFloat(float64(val))
- default:
- return fmt.Errorf("field %q should have type float64 or float32", field)
- }
- case string:
- if _, ok := fieldVal.Interface().(encoding.TextUnmarshaler); ok {
- if fieldVal.Kind() == reflect.Pointer && fieldVal.IsNil() {
- fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
- }
- return fieldVal.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val))
- }
- if unmarshaler, ok := fieldVal.Addr().Interface().(encoding.TextUnmarshaler); ok {
- return unmarshaler.UnmarshalText([]byte(val))
- }
- fieldVal := setPtr(fieldVal)
- switch {
- case fieldVal.Kind() == reflect.String:
- fieldVal.SetString(val)
- case fieldVal.Type() == reflect.TypeFor[[]byte]():
- b, err := base64.StdEncoding.DecodeString(val)
- if err != nil {
- return fmt.Errorf("field %q: bad base64", field)
- }
- fieldVal.Set(reflect.ValueOf(b))
- default:
- return fmt.Errorf("field %q should have type string (got %s)", field, fieldVal.Type())
- }
- case map[string]any:
- fieldVal := setPtr(fieldVal)
- if fieldVal.Kind() != reflect.Struct {
- return fmt.Errorf("field %q should have type struct (got %s)", field, fieldVal.Type())
- }
- if err := unpackStruct(fieldVal, fieldMap, val); err != nil {
- return err
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ if b {
+ fieldVal.SetUint(1)
+ } else {
+ fieldVal.SetUint(0)
}
- case []any:
- return fmt.Errorf("invalid repeated field")
default:
- return fmt.Errorf("unexpected AST node (unreachable)")
- }
- return nil
-}
-
-func unpackStruct(out reflect.Value, fieldMap map[structField]int, msg map[string]any) error {
- for field, val := range msg {
- fieldIdx, ok := fieldMap[structField{out.Type(), field}]
- if !ok {
- return fmt.Errorf("no field named %q", field)
- }
- fieldVal := out.Field(fieldIdx)
- if fieldVal.Kind() == reflect.Slice && fieldVal.Type() != reflect.TypeFor[[]byte]() {
- var vals []any
- if l, ok := val.([]any); ok {
- vals = l
- } else {
- vals = []any{val}
- }
- l := reflect.MakeSlice(fieldVal.Type(), len(vals), len(vals))
- for i, val := range vals {
- if err := unpackVal(l.Index(i), fieldMap, val, field); err != nil {
- return err
- }
- }
- if fieldVal.IsNil() {
- fieldVal.Set(reflect.MakeSlice(fieldVal.Type(), 0, 0))
- }
- fieldVal.Set(reflect.AppendSlice(fieldVal, l))
- continue
- }
- if err := unpackVal(fieldVal, fieldMap, val, field); err != nil {
- return err
- }
+ return p.error("field %q should have type bool", field)
}
return nil
}
@@ -786,9 +749,5 @@ func Unmarshal(data []byte, v any) error {
}
nextToken, stop := iter.Pull2(tokens(data))
defer stop()
- msg, err := (&parser{nextTok: nextToken, data: data}).parse()
- if err != nil {
- return err
- }
- return unpackStruct(val.Elem(), fields, msg)
+ return (&parser{nextTok: nextToken, data: data, fieldMap: fields}).parse(val.Elem())
}