diff --git a/bench/get_test.go b/bench/get_test.go index 753953f..63a83f5 100644 --- a/bench/get_test.go +++ b/bench/get_test.go @@ -605,8 +605,8 @@ write = 10 b.Run("Direct-ArrayGet", func(b *testing.B) { for i := 0; i < b.N; i++ { app := directData["app"].(map[string]any) - environments := app["environments"].([]any) - _ = environments[1].(string) + environments := app["environments"].([]string) + _ = environments[1] } }) diff --git a/data.go b/data.go index 33c15c5..31fcde0 100644 --- a/data.go +++ b/data.go @@ -1,32 +1,25 @@ package fin -/* - data.go - Copyright 2025 Sharkk, sharkk.net - Authors: Sky Johnson -*/ - import ( "fmt" "io" + "maps" + "slices" "strconv" ) -// Data holds a single hierarchical structure and handles parsing type Data struct { data map[string]any - dataRef *map[string]any // Reference to pooled map + dataRef *map[string]any scanner *Scanner currentObject map[string]any stack []map[string]any currentToken Token } -// NewData creates a new empty data structure func NewData() *Data { dataRef := GetMap() data := *dataRef - d := &Data{ data: data, dataRef: dataRef, @@ -36,13 +29,11 @@ func NewData() *Data { return d } -// Release frees any resources and returns them to pools func (d *Data) Release() { if d.scanner != nil { ReleaseScanner(d.scanner) d.scanner = nil } - if d.dataRef != nil { PutMap(d.dataRef) d.data = nil @@ -52,28 +43,22 @@ func (d *Data) Release() { d.stack = nil } -// GetData retrieves the entirety of the internal data map -func (d *Data) GetData() map[string]any { - return d.data -} +func (d *Data) GetData() map[string]any { return d.data } -// Get retrieves a value from the data using dot notation func (d *Data) Get(key string) (any, error) { if key == "" { return d.data, nil } - var current any = d.data start := 0 keyLen := len(key) - for i := 0; i < keyLen; i++ { + for i := range keyLen { if key[i] == '.' || i == keyLen-1 { end := i if i == keyLen-1 && key[i] != '.' { end = i + 1 } - part := key[start:end] switch node := current.(type) { @@ -92,21 +77,36 @@ func (d *Data) Get(key string) (any, error) { return nil, fmt.Errorf("array index out of bounds: %d", index) } current = node[index] + case []string: + index, err := strconv.Atoi(part) + if err != nil { + return nil, fmt.Errorf("invalid array index: %s", part) + } + if index < 0 || index >= len(node) { + return nil, fmt.Errorf("array index out of bounds: %d", index) + } + current = node[index] + case []int: + index, err := strconv.Atoi(part) + if err != nil { + return nil, fmt.Errorf("invalid array index: %s", part) + } + if index < 0 || index >= len(node) { + return nil, fmt.Errorf("array index out of bounds: %d", index) + } + current = node[index] default: return nil, fmt.Errorf("cannot access %s in non-container value", part) } - if i == keyLen-1 { return current, nil } start = i + 1 } } - return current, nil } -// GetOr retrieves a value or returns a default if not found func (d *Data) GetOr(key string, defaultValue any) any { val, err := d.Get(key) if err != nil { @@ -115,139 +115,214 @@ func (d *Data) GetOr(key string, defaultValue any) any { return val } -// GetString gets a value as string +func convertTo[T any](val any, key string, converter func(any) (T, bool)) (T, error) { + var zero T + result, ok := converter(val) + if ok { + return result, nil + } + return zero, fmt.Errorf("value for key %s cannot be converted to %T", key, zero) +} + +func toString(val any) (string, bool) { + switch v := val.(type) { + case string: + return v, true + case bool: + return strconv.FormatBool(v), true + case int: + return strconv.Itoa(v), true + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), true + } + return "", false +} + +func toBool(val any) (bool, bool) { + switch v := val.(type) { + case bool: + return v, true + case string: + if result, err := strconv.ParseBool(v); err == nil { + return result, true + } + } + return false, false +} + +func toInt(val any) (int, bool) { + switch v := val.(type) { + case int: + return v, true + case float64: + return int(v), true + case string: + if result, err := strconv.Atoi(v); err == nil { + return result, true + } + } + return 0, false +} + +func toFloat(val any) (float64, bool) { + switch v := val.(type) { + case float64: + return v, true + case int: + return float64(v), true + case string: + if result, err := strconv.ParseFloat(v, 64); err == nil { + return result, true + } + } + return 0, false +} + func (d *Data) GetString(key string) (string, error) { val, err := d.Get(key) if err != nil { return "", err } - - switch v := val.(type) { - case string: - return v, nil - case bool: - return strconv.FormatBool(v), nil - case int: - return strconv.Itoa(v), nil - case float64: - return strconv.FormatFloat(v, 'f', -1, 64), nil - default: - return "", fmt.Errorf("value for key %s cannot be converted to string", key) - } + return convertTo(val, key, toString) } -// GetBool gets a value as boolean func (d *Data) GetBool(key string) (bool, error) { val, err := d.Get(key) if err != nil { return false, err } - - switch v := val.(type) { - case bool: - return v, nil - case string: - return strconv.ParseBool(v) - default: - return false, fmt.Errorf("value for key %s cannot be converted to bool", key) - } + return convertTo(val, key, toBool) } -// GetInt gets a value as int func (d *Data) GetInt(key string) (int, error) { val, err := d.Get(key) if err != nil { return 0, err } - - switch v := val.(type) { - case int: - return v, nil - case float64: - return int(v), nil - case string: - parsed, err := strconv.Atoi(v) - return parsed, err - default: - return 0, fmt.Errorf("value for key %s cannot be converted to int", key) - } + return convertTo(val, key, toInt) } -// GetFloat gets a value as float64 func (d *Data) GetFloat(key string) (float64, error) { val, err := d.Get(key) if err != nil { return 0, err } - - switch v := val.(type) { - case float64: - return v, nil - case int: - return float64(v), nil - case string: - return strconv.ParseFloat(v, 64) - default: - return 0, fmt.Errorf("value for key %s cannot be converted to float", key) - } + return convertTo(val, key, toFloat) } -// GetArray gets a value as []any func (d *Data) GetArray(key string) ([]any, error) { val, err := d.Get(key) if err != nil { return nil, err } - if arr, ok := val.([]any); ok { - return arr, nil + switch v := val.(type) { + case []any: + return v, nil + case []string: + result := make([]any, len(v)) + for i, s := range v { + result[i] = s + } + return result, nil + case []int: + result := make([]any, len(v)) + for i, n := range v { + result[i] = n + } + return result, nil + default: + return nil, fmt.Errorf("value for key %s is not an array", key) } - return nil, fmt.Errorf("value for key %s is not an array", key) } -// GetMap gets a value as map[string]any func (d *Data) GetMap(key string) (map[string]any, error) { val, err := d.Get(key) if err != nil { return nil, err } - if m, ok := val.(map[string]any); ok { return m, nil } return nil, fmt.Errorf("value for key %s is not a map", key) } -// Error creates an error with line information from the current token -func (d *Data) Error(msg string) error { - return fmt.Errorf("line %d, column %d: %s", - d.currentToken.Line, d.currentToken.Column, msg) +func getTypedArray[T any](d *Data, key string, converter func(any) (T, bool)) ([]T, error) { + arr, err := d.GetArray(key) + if err != nil { + return nil, err + } + result := make([]T, len(arr)) + for i, v := range arr { + converted, ok := converter(v) + if !ok { + return nil, fmt.Errorf("array element at index %d cannot be converted", i) + } + result[i] = converted + } + return result, nil +} + +func (d *Data) GetStringArray(key string) ([]string, error) { + return getTypedArray(d, key, func(v any) (string, bool) { + s, ok := v.(string) + return s, ok + }) +} + +func (d *Data) GetIntArray(key string) ([]int, error) { + return getTypedArray(d, key, func(v any) (int, bool) { + switch val := v.(type) { + case int: + return val, true + case float64: + return int(val), true + } + return 0, false + }) +} + +func (d *Data) GetFloatArray(key string) ([]float64, error) { + return getTypedArray(d, key, func(v any) (float64, bool) { + switch val := v.(type) { + case float64: + return val, true + case int: + return float64(val), true + } + return 0, false + }) +} + +func (d *Data) Error(msg string) error { + return fmt.Errorf("line %d, column %d: %s", d.currentToken.Line, d.currentToken.Column, msg) +} + +func (d *Data) makeStringKey(tokenValue []byte) string { + keyBytes := GetByteSlice() + *keyBytes = append((*keyBytes)[:0], tokenValue...) + result := string(*keyBytes) + PutByteSlice(keyBytes) + return result } -// Parse parses the data from a reader func (d *Data) Parse(r io.Reader) error { d.scanner = NewScanner(r) d.currentObject = d.data err := d.parseContent() - - // Clean up scanner resources even on success if d.scanner != nil { ReleaseScanner(d.scanner) d.scanner = nil } - return err } -// nextToken gets the next meaningful token (skipping comments) func (d *Data) nextToken() (Token, error) { for { token, err := d.scanner.NextToken() if err != nil { return token, err } - - // Skip comment tokens if token.Type != TokenComment { d.currentToken = token return token, nil @@ -255,35 +330,24 @@ func (d *Data) nextToken() (Token, error) { } } -// parseContent is the main parsing function func (d *Data) parseContent() error { for { token, err := d.nextToken() if err != nil { return err } - - // Check for end of file if token.Type == TokenEOF { break } - - // We expect top level entries to be names if token.Type != TokenName { return d.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type)) } - // Get the property name - copy to create a stable key - nameBytes := GetByteSlice() - *nameBytes = append((*nameBytes)[:0], token.Value...) - name := string(*nameBytes) - PutByteSlice(nameBytes) + name := d.makeStringKey(token.Value) - // Get the next token nextToken, err := d.nextToken() if err != nil { if err == io.EOF { - // EOF after name - store as empty string d.currentObject[name] = "" break } @@ -291,65 +355,47 @@ func (d *Data) parseContent() error { } var value any - if nextToken.Type == TokenOpenBrace { - // It's a nested object/array value, err = d.parseObject() if err != nil { return err } } else { - // It's a simple value value = d.tokenToValue(nextToken) - - // Check for potential nested object - look ahead lookAhead, nextErr := d.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { - // It's a complex object that follows a value nestedValue, err := d.parseObject() if err != nil { return err } - - // Store the previous simple value in a map to add to the object if mapValue, ok := nestedValue.(map[string]any); ok { - // Create a new map value with both the simple value and the map mapRef := GetMap() newMap := *mapRef - for k, v := range mapValue { - newMap[k] = v - } - newMap["value"] = value // Store simple value under "value" key + maps.Copy(newMap, mapValue) + newMap["value"] = value value = newMap } } else if nextErr == nil && lookAhead.Type != TokenEOF { - // Put the token back if it's not EOF d.scanner.UnreadToken(lookAhead) } } - - // Store the value in the data d.currentObject[name] = value } - return nil } -// parseObject parses a map or array func (d *Data) parseObject() (any, error) { - // Default to treating as an array until we see a name isArray := true arrayRef := GetArray() arrayElements := *arrayRef - mapRef := GetMap() objectElements := *mapRef defer func() { if !isArray { - PutArray(arrayRef) // We didn't use the array + PutArray(arrayRef) } else { - PutMap(mapRef) // We didn't use the map + PutMap(mapRef) } }() @@ -362,34 +408,23 @@ func (d *Data) parseObject() (any, error) { return nil, err } - // Handle closing brace - finish current object/array if token.Type == TokenCloseBrace { if isArray && len(arrayElements) > 0 { - result := arrayElements - // Don't release the array, transfer ownership - *arrayRef = nil // Detach from pool reference + result := optimizeArray(arrayElements) + *arrayRef = nil return result, nil } result := objectElements - // Don't release the map, transfer ownership - *mapRef = nil // Detach from pool reference + *mapRef = nil return result, nil } - // Handle tokens based on type switch token.Type { case TokenName: - // Copy token value to create a stable key - keyBytes := GetByteSlice() - *keyBytes = append((*keyBytes)[:0], token.Value...) - key := string(*keyBytes) - PutByteSlice(keyBytes) - - // Look ahead to see what follows + key := d.makeStringKey(token.Value) nextToken, err := d.nextToken() if err != nil { if err == io.EOF { - // EOF after key - store as empty value objectElements[key] = "" isArray = false return objectElements, nil @@ -398,63 +433,49 @@ func (d *Data) parseObject() (any, error) { } if nextToken.Type == TokenOpenBrace { - // Nested object - isArray = false // If we see a key, it's a map + isArray = false nestedValue, err := d.parseObject() if err != nil { return nil, err } objectElements[key] = nestedValue } else { - // Key-value pair - isArray = false // If we see a key, it's a map + isArray = false value := d.tokenToValue(nextToken) objectElements[key] = value - // Check if there's an object following lookAhead, nextErr := d.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { - // Nested object after value nestedValue, err := d.parseObject() if err != nil { return nil, err } - - // Check if we need to convert the value to a map if mapValue, ok := nestedValue.(map[string]any); ok { - // Create a combined map combinedMapRef := GetMap() combinedMap := *combinedMapRef - for k, v := range mapValue { - combinedMap[k] = v - } + maps.Copy(combinedMap, mapValue) combinedMap["value"] = value objectElements[key] = combinedMap } } else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace { d.scanner.UnreadToken(lookAhead) } else if nextErr == nil && lookAhead.Type == TokenCloseBrace { - // We found the closing brace - unread it so it's handled by the main loop d.scanner.UnreadToken(lookAhead) } } case TokenString, TokenNumber, TokenBoolean: - // Array element value := d.tokenToValue(token) arrayElements = append(arrayElements, value) case TokenOpenBrace: - // Nested object/array nestedValue, err := d.parseObject() if err != nil { return nil, err } - if isArray { arrayElements = append(arrayElements, nestedValue) } else { - // If we're in an object context, this is an error return nil, d.Error("unexpected nested object without a key") } @@ -464,104 +485,61 @@ func (d *Data) parseObject() (any, error) { } } -// Load parses data from a reader func Load(r io.Reader) (*Data, error) { data := NewData() err := data.Parse(r) - if err != nil { data.Release() return nil, err } - return data, nil } -// tokenToValue converts a token to a Go value, preserving byte slices until final conversion func (d *Data) tokenToValue(token Token) any { switch token.Type { case TokenString: - // Convert to string using pooled buffer - valueBytes := GetByteSlice() - *valueBytes = append((*valueBytes)[:0], token.Value...) - result := string(*valueBytes) - PutByteSlice(valueBytes) - return result - + return d.makeStringKey(token.Value) case TokenNumber: - // Parse number valueStr := string(token.Value) - if containsChar(token.Value, '.') { - // Float + if slices.Contains(token.Value, '.') { val, _ := strconv.ParseFloat(valueStr, 64) return val } - // Integer val, _ := strconv.Atoi(valueStr) return val - case TokenBoolean: return bytesEqual(token.Value, []byte("true")) - case TokenName: - // Check if name is a special value - valueBytes := GetByteSlice() - *valueBytes = append((*valueBytes)[:0], token.Value...) - - if bytesEqual(*valueBytes, []byte("true")) { - PutByteSlice(valueBytes) - return true - } else if bytesEqual(*valueBytes, []byte("false")) { - PutByteSlice(valueBytes) - return false - } else if isDigitOrMinus((*valueBytes)[0]) { - // Try to convert to number - valueStr := string(*valueBytes) - PutByteSlice(valueBytes) - - if containsChar(token.Value, '.') { - val, err := strconv.ParseFloat(valueStr, 64) - if err == nil { + if bytesEqual(token.Value, []byte("true")) || bytesEqual(token.Value, []byte("false")) { + return bytesEqual(token.Value, []byte("true")) + } + if len(token.Value) > 0 && isDigitOrMinus(token.Value[0]) { + valueStr := string(token.Value) + if slices.Contains(token.Value, '.') { + if val, err := strconv.ParseFloat(valueStr, 64); err == nil { return val } } else { - val, err := strconv.Atoi(valueStr) - if err == nil { + if val, err := strconv.Atoi(valueStr); err == nil { return val } } return valueStr } - - // Default to string - result := string(*valueBytes) - PutByteSlice(valueBytes) - return result - - default: - return nil + return string(token.Value) } + return nil } -// Helper functions - -func isLetter(b byte) bool { - return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') -} - -func isDigit(b byte) bool { - return b >= '0' && b <= '9' -} - -func isDigitOrMinus(b byte) bool { - return isDigit(b) || b == '-' -} +func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } +func isDigit(b byte) bool { return b >= '0' && b <= '9' } +func isDigitOrMinus(b byte) bool { return isDigit(b) || b == '-' } func bytesEqual(b1, b2 []byte) bool { if len(b1) != len(b2) { return false } - for i := 0; i < len(b1); i++ { + for i := range b1 { if b1[i] != b2[i] { return false } @@ -569,11 +547,34 @@ func bytesEqual(b1, b2 []byte) bool { return true } -func containsChar(b []byte, c byte) bool { - for _, v := range b { - if v == c { - return true +func optimizeArray(arr []any) any { + if len(arr) == 0 { + return arr + } + // Check all strings + if allType[string](arr) { + result := make([]string, len(arr)) + for i, v := range arr { + result[i] = v.(string) + } + return result + } + // Check all ints + if allType[int](arr) { + result := make([]int, len(arr)) + for i, v := range arr { + result[i] = v.(int) + } + return result + } + return arr +} + +func allType[T any](arr []any) bool { + for _, v := range arr { + if _, ok := v.(T); !ok { + return false } } - return false + return true } diff --git a/scanner.go b/scanner.go index 173e3cb..24384e2 100644 --- a/scanner.go +++ b/scanner.go @@ -1,11 +1,5 @@ package fin -/* - scanner.go - Copyright 2025 Sharkk, sharkk.net - Authors: Sky Johnson -*/ - import ( "bufio" "errors" @@ -14,7 +8,6 @@ import ( "sync" ) -// Pre-declared errors to reduce allocations var ( ErrUnterminatedString = errors.New("unterminated string") ErrUnterminatedEscape = errors.New("unterminated escape sequence") @@ -23,17 +16,15 @@ var ( ErrNameStartWithLetter = errors.New("name must start with letter") ) -// Scanner handles the low-level parsing of the configuration format type Scanner struct { reader *bufio.Reader line int col int - buffer []byte // Slice to the pooled buffer - bufferRef *[]byte // Reference to the pooled buffer - token Token // Current token for unread + buffer []byte + bufferRef *[]byte + token Token } -// scannerPool helps reuse scanner objects var scannerPool = sync.Pool{ New: func() any { bufferRef := GetByteSlice() @@ -46,28 +37,35 @@ var scannerPool = sync.Pool{ }, } -// NewScanner creates a new scanner from a pool func NewScanner(r io.Reader) *Scanner { s := scannerPool.Get().(*Scanner) s.reader = bufio.NewReaderSize(r, 1024) s.line = 1 s.col = 0 - s.buffer = (*s.bufferRef)[:0] + s.resetBuffer() s.token = Token{Type: TokenError} return s } -// ReleaseScanner returns a scanner to the pool func ReleaseScanner(s *Scanner) { if s != nil { - // Clear references but keep allocated memory s.reader = nil - s.buffer = (*s.bufferRef)[:0] + s.resetBuffer() scannerPool.Put(s) } } -// ReadByte reads a single byte from the input +// Helper to reset buffer consistently +func (s *Scanner) resetBuffer() { + s.buffer = (*s.bufferRef)[:0] +} + +// Helper for creating error tokens +func (s *Scanner) errorToken(msg string, line, col int) (Token, error) { + err := fmt.Errorf("line %d, column %d: %s", line, col, msg) + return Token{Type: TokenError, Value: []byte(msg), Line: line, Column: col}, err +} + func (s *Scanner) ReadByte() (byte, error) { b, err := s.reader.ReadByte() if err == nil { @@ -81,7 +79,6 @@ func (s *Scanner) ReadByte() (byte, error) { return b, err } -// PeekByte looks at the next byte without consuming it func (s *Scanner) PeekByte() (byte, error) { b, err := s.reader.Peek(1) if err != nil { @@ -90,12 +87,10 @@ func (s *Scanner) PeekByte() (byte, error) { return b[0], nil } -// PeekBytes looks at the next n bytes without consuming them func (s *Scanner) PeekBytes(n int) ([]byte, error) { return s.reader.Peek(n) } -// UnreadByte pushes back a byte to the reader func (s *Scanner) UnreadByte() error { err := s.reader.UnreadByte() if err == nil && s.col > 0 { @@ -104,12 +99,10 @@ func (s *Scanner) UnreadByte() error { return err } -// Error creates an error with line and column information func (s *Scanner) Error(msg string) error { return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg) } -// SkipWhitespace skips whitespace characters func (s *Scanner) SkipWhitespace() error { for { b, err := s.PeekByte() @@ -119,12 +112,9 @@ func (s *Scanner) SkipWhitespace() error { if err != nil { return err } - - // Fast check for common whitespace bytes if b != ' ' && b != '\t' && b != '\n' && b != '\r' { return nil } - _, err = s.ReadByte() if err != nil { return err @@ -132,27 +122,22 @@ func (s *Scanner) SkipWhitespace() error { } } -// UnreadToken stores a token to be returned by the next call to NextToken func (s *Scanner) UnreadToken(token Token) { s.token = token } -// NextToken scans and returns the next token func (s *Scanner) NextToken() (Token, error) { if s.token.Type != TokenError { - // We have a stored token token := s.token - s.token = Token{Type: TokenError} // Reset + s.token = Token{Type: TokenError} return token, nil } - // Skip whitespace - err := s.SkipWhitespace() - if err != nil { + if err := s.SkipWhitespace(); err != nil { if err == io.EOF { return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil } - return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err + return s.errorToken(err.Error(), s.line, s.col) } b, err := s.PeekByte() @@ -160,69 +145,50 @@ func (s *Scanner) NextToken() (Token, error) { if err == io.EOF { return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil } - return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err + return s.errorToken(err.Error(), s.line, s.col) } - // Record start position for error reporting startLine, startColumn := s.line, s.col - // Process based on first character switch { case b == '{': - _, _ = s.ReadByte() // consume open brace + _, _ = s.ReadByte() return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil - case b == '}': - _, _ = s.ReadByte() // consume close brace + _, _ = s.ReadByte() return Token{Type: TokenCloseBrace, Line: startLine, Column: startColumn}, nil - case b == '-': - // Could be a comment or a negative number peekBytes, err := s.PeekBytes(2) if err == nil && len(peekBytes) == 2 && peekBytes[1] == '-' { - err = s.scanComment() - if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + if err := s.scanComment(); err != nil { + return s.errorToken(err.Error(), startLine, startColumn) } return Token{Type: TokenComment, Line: startLine, Column: startColumn}, nil } - - // Check if it's a negative number if err == nil && len(peekBytes) == 2 && isDigit(peekBytes[1]) { return s.scanNumber(startLine, startColumn) } - - // Just a single dash - _, _ = s.ReadByte() // consume dash - return Token{Type: TokenError, Value: []byte("unexpected '-'")}, - s.Error("unexpected '-'") - + _, _ = s.ReadByte() + return s.errorToken("unexpected '-'", startLine, startColumn) case b == '"': return s.scanString(startLine, startColumn) - case isLetter(b): return s.scanName(startLine, startColumn) - case isDigit(b): return s.scanNumber(startLine, startColumn) - default: - _, _ = s.ReadByte() // consume the unexpected character - return Token{Type: TokenError, Value: []byte(fmt.Sprintf("unexpected character: %c", b)), Line: startLine, Column: startColumn}, - s.Error(fmt.Sprintf("unexpected character: %c", b)) + _, _ = s.ReadByte() + msg := fmt.Sprintf("unexpected character: %c", b) + return s.errorToken(msg, startLine, startColumn) } } -// scanComment processes a comment func (s *Scanner) scanComment() error { - // Consume the first dash - _, err := s.ReadByte() + _, err := s.ReadByte() // consume first dash if err != nil { return err } - - // Check for second dash - b, err := s.ReadByte() + b, err := s.ReadByte() // consume second dash if err != nil { return err } @@ -230,12 +196,11 @@ func (s *Scanner) scanComment() error { return ErrInvalidComment } - // Check for block comment [[ + // Check for block comment if b1, err := s.PeekByte(); err == nil && b1 == '[' { - _, _ = s.ReadByte() // consume first [ + _, _ = s.ReadByte() if b2, err := s.PeekByte(); err == nil && b2 == '[' { - _, _ = s.ReadByte() // consume second [ - // Process block comment + _, _ = s.ReadByte() for { b, err := s.ReadByte() if err != nil { @@ -243,7 +208,7 @@ func (s *Scanner) scanComment() error { } if b == ']' { if n, err := s.PeekByte(); err == nil && n == ']' { - _, _ = s.ReadByte() // consume second ] + _, _ = s.ReadByte() return nil } } @@ -251,7 +216,7 @@ func (s *Scanner) scanComment() error { } } - // Line comment - consume until newline or EOF + // Line comment for { b, err := s.ReadByte() if err == io.EOF { @@ -266,38 +231,29 @@ func (s *Scanner) scanComment() error { } } -// scanString scans a quoted string func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { - // Reset buffer - s.buffer = (*s.bufferRef)[:0] - - // Consume opening quote - _, err := s.ReadByte() + s.resetBuffer() + _, err := s.ReadByte() // consume opening quote if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return s.errorToken(err.Error(), startLine, startColumn) } for { b, err := s.ReadByte() if err != nil { - return Token{Type: TokenError, Value: []byte(ErrUnterminatedString.Error())}, ErrUnterminatedString + return s.errorToken(ErrUnterminatedString.Error(), startLine, startColumn) } - if b == '"' { break } - - // Handle escape sequences if b == '\\' { escaped, err := s.ReadByte() if err != nil { - return Token{Type: TokenError, Value: []byte(ErrUnterminatedEscape.Error())}, ErrUnterminatedEscape + return s.errorToken(ErrUnterminatedEscape.Error(), startLine, startColumn) } switch escaped { - case '"': - s.buffer = append(s.buffer, '"') - case '\\': - s.buffer = append(s.buffer, '\\') + case '"', '\\': + s.buffer = append(s.buffer, escaped) case 'n': s.buffer = append(s.buffer, '\n') case 't': @@ -310,39 +266,27 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { } } - // Return token with buffer value - important: consumer must copy if needed - return Token{ - Type: TokenString, - Value: s.buffer, - Line: startLine, - Column: startColumn, - }, nil + return Token{Type: TokenString, Value: s.buffer, Line: startLine, Column: startColumn}, nil } -// scanName scans an identifier func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { - // Reset buffer - s.buffer = (*s.bufferRef)[:0] - - // Read first character + s.resetBuffer() b, err := s.ReadByte() if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return s.errorToken(err.Error(), startLine, startColumn) } - if !isLetter(b) { - return Token{Type: TokenError, Value: []byte(ErrNameStartWithLetter.Error())}, ErrNameStartWithLetter + return s.errorToken(ErrNameStartWithLetter.Error(), startLine, startColumn) } s.buffer = append(s.buffer, b) - // Read rest of name for { b, err := s.PeekByte() if err == io.EOF { break } if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return s.errorToken(err.Error(), startLine, startColumn) } if !isLetter(b) && !isDigit(b) && b != '_' { break @@ -351,33 +295,22 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { _, _ = s.ReadByte() } - // Check if it's a boolean - use direct byte comparison tokenType := TokenName if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) { tokenType = TokenBoolean } - return Token{ - Type: tokenType, - Value: s.buffer, // Direct buffer reference - consumer must copy! - Line: startLine, - Column: startColumn, - }, nil + return Token{Type: tokenType, Value: s.buffer, Line: startLine, Column: startColumn}, nil } -// scanNumber scans a numeric value func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { - // Reset buffer - s.buffer = (*s.bufferRef)[:0] - - // Read first character (might be a minus sign or digit) + s.resetBuffer() b, err := s.ReadByte() if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return s.errorToken(err.Error(), startLine, startColumn) } s.buffer = append(s.buffer, b) - // Scan the rest of the number hasDot := false for { b, err := s.PeekByte() @@ -385,9 +318,8 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { if err == io.EOF { break } - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return s.errorToken(err.Error(), startLine, startColumn) } - if b == '.' && !hasDot { hasDot = true _, _ = s.ReadByte() @@ -400,10 +332,5 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { } } - return Token{ - Type: TokenNumber, - Value: s.buffer, // Direct buffer reference - consumer must copy! - Line: startLine, - Column: startColumn, - }, nil + return Token{Type: TokenNumber, Value: s.buffer, Line: startLine, Column: startColumn}, nil } diff --git a/writer.go b/writer.go index 730c215..3e6bf5c 100644 --- a/writer.go +++ b/writer.go @@ -7,6 +7,7 @@ package fin */ import ( + "fmt" "io" "strconv" ) @@ -16,289 +17,196 @@ func (d *Data) Write(w io.Writer) error { return writeMap(w, d.data, 0) } -// Save writes data to a writer (standalone function) func Save(w io.Writer, d *Data) error { return d.Write(w) } -// writeMap writes a map at the given indent level -func writeMap(w io.Writer, data map[string]any, level int) error { - for key, value := range data { - // Write indentation - for i := 0; i < level; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - // Write key - if _, err := io.WriteString(w, key); err != nil { - return err - } - - // Special case: combined value+object - if m, ok := value.(map[string]any); ok && len(m) > 1 { - if simpleValue, hasValue := m["value"]; hasValue { - // Write simple value first - if err := writeSimpleValue(w, simpleValue); err != nil { - return err - } - - // Write object portion - if _, err := io.WriteString(w, " {\n"); err != nil { - return err - } - - for k, v := range m { - if k != "value" { - for i := 0; i < level+1; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - if _, err := io.WriteString(w, k); err != nil { - return err - } - - if err := writeValueWithNewline(w, v, level+1); err != nil { - return err - } - } - } - - for i := 0; i < level; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - if _, err := io.WriteString(w, "}\n"); err != nil { - return err - } - - continue - } - } - - // Regular value handling - if err := writeValueWithNewline(w, value, level); err != nil { +// Helper for writing indentation +func writeIndent(w io.Writer, level int) error { + for range level { + if _, err := w.Write([]byte{'\t'}); err != nil { return err } } - return nil } -// writeValueWithNewline writes a value and adds a newline -func writeValueWithNewline(w io.Writer, value any, level int) error { - if err := writeValue(w, value, level); err != nil { - return err - } - - if _, err := io.WriteString(w, "\n"); err != nil { - return err - } - - return nil -} - -// writeValue writes a value with appropriate formatting -func writeValue(w io.Writer, value any, level int) error { - // Space after key - if _, err := w.Write([]byte{' '}); err != nil { - return err - } - - switch v := value.(type) { - case nil: - return nil - - case string: - // Quote strings - if _, err := io.WriteString(w, "\""); err != nil { - return err - } - - if err := writeEscapedString(w, v); err != nil { - return err - } - - if _, err := io.WriteString(w, "\""); err != nil { - return err - } - - return nil +// Helper for writing numbers using pooled buffers +func writeNumber(w io.Writer, val any) error { + buffer := GetByteSlice() + defer PutByteSlice(buffer) + switch v := val.(type) { case int: - // Use byte slice pool for better performance - buffer := GetByteSlice() *buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10) - _, err := w.Write(*buffer) - PutByteSlice(buffer) - return err - case float64: - buffer := GetByteSlice() *buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64) - _, err := w.Write(*buffer) - PutByteSlice(buffer) - return err - - case bool: - if v { - _, err := io.WriteString(w, "true") - return err - } - _, err := io.WriteString(w, "false") - return err - - case map[string]any: - if _, err := io.WriteString(w, "{\n"); err != nil { - return err - } - - if err := writeMap(w, v, level+1); err != nil { - return err - } - - for i := 0; i < level; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - if _, err := io.WriteString(w, "}"); err != nil { - return err - } - - return nil - - case []any: - if _, err := io.WriteString(w, "{\n"); err != nil { - return err - } - - if err := writeArray(w, v, level+1); err != nil { - return err - } - - for i := 0; i < level; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - if _, err := io.WriteString(w, "}"); err != nil { - return err - } - - return nil - default: - // Fall back for any other types - buffer := GetByteSlice() - *buffer = append((*buffer)[:0], []byte(strconv.FormatInt(int64(v.(int)), 10))...) - _, err := w.Write(*buffer) - PutByteSlice(buffer) - return err + return fmt.Errorf("not a number type") } + + _, err := w.Write(*buffer) + return err } -// writeArray writes array elements -func writeArray(w io.Writer, array []any, level int) error { - for _, item := range array { - for i := 0; i < level; i++ { - if _, err := w.Write([]byte{'\t'}); err != nil { - return err - } - } - - if err := writeValue(w, item, level); err != nil { - return err - } - - if _, err := io.WriteString(w, "\n"); err != nil { +// Consolidated string writing with escaping +func writeString(w io.Writer, s string, quote bool) error { + if quote { + if _, err := io.WriteString(w, "\""); err != nil { return err } } - return nil -} - -// writeEscapedString writes a string with escape sequences -func writeEscapedString(w io.Writer, s string) error { - for i := 0; i < len(s); i++ { + for i := range len(s) { c := s[i] + var escaped string switch c { case '"': - if _, err := io.WriteString(w, "\\\""); err != nil { - return err - } + escaped = "\\\"" case '\\': - if _, err := io.WriteString(w, "\\\\"); err != nil { - return err - } + escaped = "\\\\" case '\n': - if _, err := io.WriteString(w, "\\n"); err != nil { - return err - } + escaped = "\\n" case '\t': - if _, err := io.WriteString(w, "\\t"); err != nil { - return err - } + escaped = "\\t" default: if _, err := w.Write([]byte{c}); err != nil { return err } + continue + } + if _, err := io.WriteString(w, escaped); err != nil { + return err } } + if quote { + if _, err := io.WriteString(w, "\""); err != nil { + return err + } + } return nil } -// writeSimpleValue writes a simple value without newline -func writeSimpleValue(w io.Writer, value any) error { - if _, err := w.Write([]byte{' '}); err != nil { - return err +// Unified value writing logic +func writeValue(w io.Writer, value any, level int, addSpace, addNewline bool) error { + if addSpace { + if _, err := w.Write([]byte{' '}); err != nil { + return err + } } switch v := value.(type) { + case nil: + // Do nothing for nil case string: - if _, err := io.WriteString(w, "\""); err != nil { + if err := writeString(w, v, true); err != nil { return err } - - if err := writeEscapedString(w, v); err != nil { + case int, float64: + if err := writeNumber(w, v); err != nil { return err } - - if _, err := io.WriteString(w, "\""); err != nil { - return err - } - - case int: - buffer := GetByteSlice() - *buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10) - _, err := w.Write(*buffer) - PutByteSlice(buffer) - return err - - case float64: - buffer := GetByteSlice() - *buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64) - _, err := w.Write(*buffer) - PutByteSlice(buffer) - return err - case bool: + val := "false" if v { - _, err := io.WriteString(w, "true") + val = "true" + } + if _, err := io.WriteString(w, val); err != nil { + return err + } + case map[string]any: + if _, err := io.WriteString(w, "{\n"); err != nil { + return err + } + if err := writeMap(w, v, level+1); err != nil { + return err + } + if err := writeIndent(w, level); err != nil { + return err + } + if _, err := io.WriteString(w, "}"); err != nil { + return err + } + case []any: + if _, err := io.WriteString(w, "{\n"); err != nil { + return err + } + if err := writeArray(w, v, level+1); err != nil { + return err + } + if err := writeIndent(w, level); err != nil { + return err + } + if _, err := io.WriteString(w, "}"); err != nil { return err } - _, err := io.WriteString(w, "false") - return err } + if addNewline { + if _, err := io.WriteString(w, "\n"); err != nil { + return err + } + } + return nil +} + +func writeMap(w io.Writer, data map[string]any, level int) error { + for key, value := range data { + if err := writeIndent(w, level); err != nil { + return err + } + if _, err := io.WriteString(w, key); err != nil { + return err + } + + // Handle combined value+object case + if m, ok := value.(map[string]any); ok && len(m) > 1 { + if simpleValue, hasValue := m["value"]; hasValue { + if err := writeValue(w, simpleValue, level, true, false); err != nil { + return err + } + if _, err := io.WriteString(w, " {\n"); err != nil { + return err + } + for k, v := range m { + if k != "value" { + if err := writeIndent(w, level+1); err != nil { + return err + } + if _, err := io.WriteString(w, k); err != nil { + return err + } + if err := writeValue(w, v, level+1, true, true); err != nil { + return err + } + } + } + if err := writeIndent(w, level); err != nil { + return err + } + if _, err := io.WriteString(w, "}\n"); err != nil { + return err + } + continue + } + } + + if err := writeValue(w, value, level, true, true); err != nil { + return err + } + } + return nil +} + +func writeArray(w io.Writer, array []any, level int) error { + for _, item := range array { + if err := writeIndent(w, level); err != nil { + return err + } + if err := writeValue(w, item, level, true, true); err != nil { + return err + } + } return nil }