diff --git a/bench/bench_test.go b/bench/bench_test.go index 08100cf..4a3bfdf 100644 --- a/bench/bench_test.go +++ b/bench/bench_test.go @@ -15,10 +15,10 @@ import ( func BenchmarkSmallConfig(b *testing.B) { // Small config with just a few key-value pairs smallConfig := ` - host = "localhost" - port = 8080 - debug = true - timeout = 30 + host "localhost" + port 8080 + debug true + timeout 30 ` b.ResetTimer() @@ -35,21 +35,21 @@ func BenchmarkMediumConfig(b *testing.B) { // Medium config with nested structures and arrays mediumConfig := ` app { - name = "TestApp" - version = "1.0.0" - enableLogging = true + name "TestApp" + version "1.0.0" + enableLogging true } database { - host = "db.example.com" - port = 5432 + host "db.example.com" + port 5432 credentials { - username = "admin" - password = "secure123" + username "admin" + password "secure123" } } - features = { + features { "authentication" "authorization" "reporting" @@ -57,18 +57,18 @@ func BenchmarkMediumConfig(b *testing.B) { } timeouts { - connect = 5 - read = 10 - write = 10 - idle = 60 + connect 5 + read 10 + write 10 + idle 60 } -- Comments to add some parsing overhead endpoints { - api = "/api/v1" - web = "/web" - admin = "/admin" - health = "/health" + api "/api/v1" + web "/web" + admin "/admin" + health "/health" } ` @@ -86,38 +86,38 @@ func BenchmarkLargeConfig(b *testing.B) { // Simpler large config with careful bracket matching largeConfig := ` application { - name = "EnterpriseApp" - version = "2.5.1" - environment = "production" - debug = false - maxConnections = 1000 - timeout = 30 - retryCount = 3 - logLevel = "info" + name "EnterpriseApp" + version "2.5.1" + environment "production" + debug false + maxConnections 1000 + timeout 30 + retryCount 3 + logLevel "info" } -- Database cluster configuration databases { primary { - host = "primary-db.example.com" - port = 5432 - maxConnections = 100 + host "primary-db.example.com" + port 5432 + maxConnections 100 credentials { - username = "app_user" - password = "super_secret" - ssl = true - timeout = 5 + username "app_user" + password "super_secret" + ssl true + timeout 5 } } replica { - host = "replica-db.example.com" - port = 5432 - maxConnections = 200 + host "replica-db.example.com" + port 5432 + maxConnections 200 credentials { - username = "read_user" - password = "read_only_pw" - ssl = true + username "read_user" + password "read_only_pw" + ssl true } } } @@ -140,7 +140,7 @@ func BenchmarkLargeConfig(b *testing.B) { for i := 1; i <= 50; i++ { builder.WriteString("\n\t\tsetting") builder.WriteString(strconv.Itoa(i)) - builder.WriteString(" = ") + builder.WriteString(" ") builder.WriteString(strconv.Itoa(i * 10)) } @@ -150,7 +150,7 @@ func BenchmarkLargeConfig(b *testing.B) { roles { admin { - permissions = { + permissions { "read" "write" "delete" @@ -158,7 +158,7 @@ func BenchmarkLargeConfig(b *testing.B) { } } user { - permissions = { + permissions { "read" "write" } @@ -619,10 +619,10 @@ permissions = ["read", "write"] // Value Retrieval Benchmarks for Custom Config Format func BenchmarkRetrieveSimpleValues(b *testing.B) { configData := ` - host = "localhost" - port = 8080 - debug = true - timeout = 30 + host "localhost" + port 8080 + debug true + timeout 30 ` // Parse once before benchmarking retrieval @@ -663,19 +663,19 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) { func BenchmarkRetrieveNestedValues(b *testing.B) { configData := ` app { - name = "TestApp" - version = "1.0.0" + name "TestApp" + version "1.0.0" settings { - enableLogging = true - maxConnections = 100 + enableLogging true + maxConnections 100 } } database { - host = "db.example.com" - port = 5432 + host "db.example.com" + port 5432 credentials { - username = "admin" - password = "secure123" + username "admin" + password "secure123" } } ` @@ -715,13 +715,13 @@ func BenchmarkRetrieveNestedValues(b *testing.B) { func BenchmarkRetrieveArrayValues(b *testing.B) { configData := ` - features = { + features { "authentication" "authorization" "reporting" "analytics" } - numbers = { + numbers { 1 2 3 @@ -767,19 +767,19 @@ func BenchmarkRetrieveArrayValues(b *testing.B) { func BenchmarkRetrieveMixedValues(b *testing.B) { configData := ` app { - name = "TestApp" - version = "1.0.0" - environments = { + name "TestApp" + version "1.0.0" + environments { "development" "testing" "production" } limits { - requests = 1000 - connections = 100 + requests 1000 + connections 100 timeouts { - read = 5 - write = 10 + read 5 + write 10 } } } @@ -1065,17 +1065,17 @@ func BenchmarkComplexValueRetrieval(b *testing.B) { // Setup complex config with deep nesting and arrays for all formats customConfig := ` app { - name = "TestApp" - version = "1.0.0" + name "TestApp" + version "1.0.0" settings { - enableLogging = true - maxConnections = 100 + enableLogging true + maxConnections 100 timeouts { - read = 5 - write = 10 + read 5 + write 10 } } - environments = { + environments { "development" "testing" "production" diff --git a/config.go b/config.go index a19ff50..725d596 100644 --- a/config.go +++ b/config.go @@ -6,18 +6,10 @@ import ( "strconv" ) -// ParseState represents a parsing level state -type ParseState struct { - object map[string]any - arrayElements []any - isArray bool - currentKey string - expectValue bool -} - // Config holds a single hierarchical structure like JSON and handles parsing type Config struct { data map[string]any + dataRef *map[string]any // Reference to pooled map scanner *Scanner currentObject map[string]any stack []map[string]any @@ -26,14 +18,34 @@ type Config struct { // NewConfig creates a new empty config func NewConfig() *Config { + dataRef := GetMap() + data := *dataRef + cfg := &Config{ - data: make(map[string]any, 16), // Pre-allocate with expected capacity - stack: make([]map[string]any, 0, 8), + data: data, + dataRef: dataRef, + stack: make([]map[string]any, 0, 8), } cfg.currentObject = cfg.data return cfg } +// Release frees any resources and returns them to pools +func (c *Config) Release() { + if c.scanner != nil { + ReleaseScanner(c.scanner) + c.scanner = nil + } + + if c.dataRef != nil { + PutMap(c.dataRef) + c.data = nil + c.dataRef = nil + } + c.currentObject = nil + c.stack = nil +} + // Get retrieves a value from the config using dot notation func (c *Config) Get(key string) (any, error) { if key == "" { @@ -56,7 +68,6 @@ func (c *Config) Get(key string) (any, error) { // Handle current node based on its type switch node := current.(type) { case map[string]any: - // Simple map lookup val, ok := node[part] if !ok { return nil, fmt.Errorf("key %s not found", part) @@ -64,7 +75,6 @@ func (c *Config) Get(key string) (any, error) { current = val case []any: - // Must be numeric index index, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid array index: %s", part) @@ -78,7 +88,6 @@ func (c *Config) Get(key string) (any, error) { return nil, fmt.Errorf("cannot access %s in non-container value", part) } - // If we've processed the entire key, return the current value if i == len(key)-1 || (i < len(key)-1 && key[i] == '.' && end == i) { if i == len(key)-1 { return current, nil @@ -203,8 +212,6 @@ func (c *Config) GetMap(key string) (map[string]any, error) { return nil, fmt.Errorf("value for key %s is not a map", key) } -// --- Parser Methods (integrated into Config) --- - // Error creates an error with line information from the current token func (c *Config) Error(msg string) error { return fmt.Errorf("line %d, column %d: %s", @@ -257,244 +264,198 @@ func (c *Config) parseContent() error { // We expect top level entries to be names if token.Type != TokenName { - return c.Error("expected name at top level") + return c.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 := token.Value - name := string(nameBytes) + nameBytes := GetByteSlice() + *nameBytes = append((*nameBytes)[:0], token.Value...) + name := string(*nameBytes) + PutByteSlice(nameBytes) - // Get the next token (should be = or {) - token, err = c.nextToken() + // Get the next token + nextToken, err := c.nextToken() if err != nil { + if err == io.EOF { + // EOF after name - store as empty string + c.currentObject[name] = "" + break + } return err } var value any - if token.Type == TokenEquals { - // It's a standard key=value assignment - value, err = c.parseValue() - if err != nil { - return err - } - } else if token.Type == TokenOpenBrace { - // It's a map/array without '=' + if nextToken.Type == TokenOpenBrace { + // It's a nested object/array value, err = c.parseObject() if err != nil { return err } } else { - return c.Error("expected '=' or '{' after name") + // It's a simple value + value = c.tokenToValue(nextToken) + + // Check for potential nested object - look ahead + lookAhead, nextErr := c.nextToken() + if nextErr == nil && lookAhead.Type == TokenOpenBrace { + // It's a complex object that follows a value + nestedValue, err := c.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 + value = newMap + } + } else if nextErr == nil && lookAhead.Type != TokenEOF { + // Put the token back if it's not EOF + c.scanner.UnreadToken(lookAhead) + } } // Store the value in the config - if mapValue, ok := value.(map[string]any); ok { - // Add an entry in current object - newMap := make(map[string]any, 8) // Pre-allocate with capacity - c.currentObject[name] = newMap - - // Process the map contents - c.stack = append(c.stack, c.currentObject) - c.currentObject = newMap - - // Copy values from scanned map to our object - for k, v := range mapValue { - c.currentObject[k] = v - } - - // Restore parent object - n := len(c.stack) - if n > 0 { - c.currentObject = c.stack[n-1] - c.stack = c.stack[:n-1] - } - } else { - // Direct storage for primitives and arrays - c.currentObject[name] = value - } + c.currentObject[name] = value } return nil } -// parseValue parses a value after an equals sign -func (c *Config) parseValue() (any, error) { - token, err := c.nextToken() - if err != nil { - return nil, err - } - - switch token.Type { - case TokenString: - // Copy the value for string stability - return string(token.Value), nil - - case TokenNumber: - strValue := string(token.Value) - for i := 0; i < len(strValue); i++ { - if strValue[i] == '.' { - // It's a float - val, err := strconv.ParseFloat(strValue, 64) - if err != nil { - return nil, c.Error(fmt.Sprintf("invalid float: %s", strValue)) - } - return val, nil - } - } - // It's an integer - val, err := strconv.ParseInt(strValue, 10, 64) - if err != nil { - return nil, c.Error(fmt.Sprintf("invalid integer: %s", strValue)) - } - return val, nil - - case TokenBoolean: - return bytesEqual(token.Value, []byte("true")), nil - - case TokenOpenBrace: - // It's a map or array - return c.parseObject() - - case TokenName: - // Treat as a string value - copy to create a stable string - return string(token.Value), nil - - default: - return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type)) - } -} - // parseObject parses a map or array func (c *Config) parseObject() (any, error) { - // Initialize stack with first state - stack := []*ParseState{{ - object: make(map[string]any, 8), - arrayElements: make([]any, 0, 8), - isArray: true, - }} + // Default to treating as an array until we see a name + isArray := true + arrayRef := GetArray() + arrayElements := *arrayRef - for len(stack) > 0 { - // Get current state from top of stack - current := stack[len(stack)-1] + mapRef := GetMap() + objectElements := *mapRef + defer func() { + if !isArray { + PutArray(arrayRef) // We didn't use the array + } else { + PutMap(mapRef) // We didn't use the map + } + }() + + for { token, err := c.nextToken() if err != nil { + if err == io.EOF { + return nil, fmt.Errorf("unexpected EOF in object/array") + } return nil, err } // Handle closing brace - finish current object/array if token.Type == TokenCloseBrace { - // Determine result based on what we've collected - var result any - if current.isArray && len(current.object) == 0 { - result = current.arrayElements - } else { - result = current.object - } - - // Pop the stack - stack = stack[:len(stack)-1] - - // If stack is empty, we're done with the root object - if len(stack) == 0 { + if isArray && len(arrayElements) > 0 { + result := arrayElements + // Don't release the array, transfer ownership + *arrayRef = nil // Detach from pool reference return result, nil } - - // Otherwise, add result to parent - parent := stack[len(stack)-1] - if parent.expectValue { - parent.object[parent.currentKey] = result - parent.expectValue = false - } else { - parent.arrayElements = append(parent.arrayElements, result) - } - continue + result := objectElements + // Don't release the map, transfer ownership + *mapRef = nil // Detach from pool reference + return result, nil } // Handle tokens based on type switch token.Type { case TokenName: - name := string(token.Value) + // Copy token value to create a stable key + keyBytes := GetByteSlice() + *keyBytes = append((*keyBytes)[:0], token.Value...) + key := string(*keyBytes) + PutByteSlice(keyBytes) - // Look ahead to determine context + // Look ahead to see what follows nextToken, err := c.nextToken() + if err != nil { + if err == io.EOF { + // EOF after key - store as empty value + objectElements[key] = "" + isArray = false + return objectElements, nil + } + return nil, err + } + + if nextToken.Type == TokenOpenBrace { + // Nested object + isArray = false // If we see a key, it's a map + nestedValue, err := c.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 + value := c.tokenToValue(nextToken) + objectElements[key] = value + + // Check if there's an object following + lookAhead, nextErr := c.nextToken() + if nextErr == nil && lookAhead.Type == TokenOpenBrace { + // Nested object after value + nestedValue, err := c.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 + } + combinedMap["value"] = value + objectElements[key] = combinedMap + } + } else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace { + c.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 + c.scanner.UnreadToken(lookAhead) + } + } + + case TokenString, TokenNumber, TokenBoolean: + // Array element + value := c.tokenToValue(token) + arrayElements = append(arrayElements, value) + + case TokenOpenBrace: + // Nested object/array + nestedValue, err := c.parseObject() if err != nil { return nil, err } - if nextToken.Type == TokenEquals { - // Key-value pair - current.isArray = false - current.currentKey = name - current.expectValue = true - - // Parse the value - valueToken, err := c.nextToken() - if err != nil { - return nil, err - } - - if valueToken.Type == TokenOpenBrace { - // Push new state for nested object/array - newState := &ParseState{ - object: make(map[string]any, 8), - arrayElements: make([]any, 0, 8), - isArray: true, - } - stack = append(stack, newState) - } else { - // Handle primitive value - value := c.tokenToValue(valueToken) - current.object[name] = value - } - } else if nextToken.Type == TokenOpenBrace { - // Nested object with name - current.isArray = false - current.currentKey = name - current.expectValue = true - - // Push new state for nested object - newState := &ParseState{ - object: make(map[string]any, 8), - arrayElements: make([]any, 0, 8), - isArray: true, - } - stack = append(stack, newState) + if isArray { + arrayElements = append(arrayElements, nestedValue) } else { - // Array element - c.scanner.UnreadToken(nextToken) - - // Convert name to appropriate type - value := c.convertNameValue(name) - current.arrayElements = append(current.arrayElements, value) + // If we're in an object context, this is an error + return nil, c.Error("unexpected nested object without a key") } - case TokenString, TokenNumber, TokenBoolean: - value := c.tokenToValue(token) - - if current.expectValue { - current.object[current.currentKey] = value - current.expectValue = false - } else { - current.arrayElements = append(current.arrayElements, value) - } - - case TokenOpenBrace: - // New nested object/array - newState := &ParseState{ - object: make(map[string]any, 8), - arrayElements: make([]any, 0, 8), - isArray: true, - } - stack = append(stack, newState) - default: - return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type)) + return nil, c.Error(fmt.Sprintf("unexpected token type: %v", token.Type)) } } - - return nil, fmt.Errorf("unexpected end of parsing") } // Load parses a config from a reader @@ -503,13 +464,80 @@ func Load(r io.Reader) (*Config, error) { err := config.Parse(r) if err != nil { + config.Release() return nil, err } return config, nil } -// Helpers +// tokenToValue converts a token to a Go value, preserving byte slices until final conversion +func (c *Config) 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 + + case TokenNumber: + // Parse number + valueStr := string(token.Value) + if containsChar(token.Value, '.') { + // Float + val, _ := strconv.ParseFloat(valueStr, 64) + return val + } + // Integer + val, _ := strconv.ParseInt(valueStr, 10, 64) + 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 { + return val + } + } else { + val, err := strconv.ParseInt(valueStr, 10, 64) + if err == nil { + return val + } + } + return valueStr + } + + // Default to string + result := string(*valueBytes) + PutByteSlice(valueBytes) + return result + + default: + return nil + } +} + +// Helper functions func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') @@ -519,77 +547,27 @@ func isDigit(b byte) bool { return b >= '0' && b <= '9' } -// ParseNumber converts a string to a number (int64 or float64) -func ParseNumber(s string) (any, error) { - // Check if it has a decimal point - for i := 0; i < len(s); i++ { - if s[i] == '.' { - // It's a float - return strconv.ParseFloat(s, 64) - } - } - // It's an integer - return strconv.ParseInt(s, 10, 64) +func isDigitOrMinus(b byte) bool { + return isDigit(b) || b == '-' } -// bytesEqual compares a byte slice with either a string or byte slice -func bytesEqual(b []byte, s []byte) bool { - if len(b) != len(s) { +func bytesEqual(b1, b2 []byte) bool { + if len(b1) != len(b2) { return false } - for i := 0; i < len(b); i++ { - if b[i] != s[i] { + for i := 0; i < len(b1); i++ { + if b1[i] != b2[i] { return false } } return true } -// isDigitOrMinus checks if a string starts with a digit or minus sign -func isDigitOrMinus(s string) bool { - if len(s) == 0 { - return false - } - return isDigit(s[0]) || (s[0] == '-' && len(s) > 1 && isDigit(s[1])) -} - -// parseStringAsNumber tries to parse a string as a number (float or int) -func parseStringAsNumber(s string) (any, error) { - // Check if it has a decimal point - for i := 0; i < len(s); i++ { - if s[i] == '.' { - // It's a float - return strconv.ParseFloat(s, 64) +func containsChar(b []byte, c byte) bool { + for _, v := range b { + if v == c { + return true } } - // It's an integer - return strconv.ParseInt(s, 10, 64) -} - -func (c *Config) tokenToValue(token Token) any { - switch token.Type { - case TokenString: - return string(token.Value) - case TokenNumber: - val, _ := parseStringAsNumber(string(token.Value)) - return val - case TokenBoolean: - return bytesEqual(token.Value, []byte("true")) - default: - return string(token.Value) - } -} - -func (c *Config) convertNameValue(name string) any { - if name == "true" { - return true - } else if name == "false" { - return false - } else if isDigitOrMinus(name) { - val, err := parseStringAsNumber(name) - if err == nil { - return val - } - } - return name + return false } diff --git a/pool.go b/pool.go new file mode 100644 index 0000000..10bcfe8 --- /dev/null +++ b/pool.go @@ -0,0 +1,71 @@ +package config + +import ( + "sync" +) + +// byteSlicePool helps reuse byte slices +var byteSlicePool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 0, 128) + return &b + }, +} + +// GetByteSlice gets a byte slice from the pool +func GetByteSlice() *[]byte { + return byteSlicePool.Get().(*[]byte) +} + +// PutByteSlice returns a byte slice to the pool +func PutByteSlice(b *[]byte) { + if b != nil { + *b = (*b)[:0] // Clear but keep capacity + byteSlicePool.Put(b) + } +} + +// mapPool helps reuse maps for config objects +var mapPool = sync.Pool{ + New: func() interface{} { + m := make(map[string]any, 16) + return &m + }, +} + +// GetMap gets a map from the pool +func GetMap() *map[string]any { + return mapPool.Get().(*map[string]any) +} + +// PutMap returns a map to the pool after clearing it +func PutMap(m *map[string]any) { + if m != nil { + // Clear the map + for k := range *m { + delete(*m, k) + } + mapPool.Put(m) + } +} + +// arrayPool helps reuse slices +var arrayPool = sync.Pool{ + New: func() interface{} { + a := make([]any, 0, 8) + return &a + }, +} + +// GetArray gets a slice from the pool +func GetArray() *[]any { + return arrayPool.Get().(*[]any) +} + +// PutArray returns a slice to the pool +func PutArray(a *[]any) { + if a != nil { + *a = (*a)[:0] // Clear but keep capacity + arrayPool.Put(a) + } +} diff --git a/scanner.go b/scanner.go index c70584f..68617b7 100644 --- a/scanner.go +++ b/scanner.go @@ -19,20 +19,23 @@ var ( // Scanner handles the low-level parsing of the configuration format type Scanner struct { - reader *bufio.Reader - line int - col int - buffer []byte - token Token // Current token for unread + 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 } // scannerPool helps reuse scanner objects var scannerPool = sync.Pool{ New: func() interface{} { + bufferRef := GetByteSlice() return &Scanner{ - line: 1, - col: 0, - buffer: make([]byte, 0, 128), + line: 1, + col: 0, + bufferRef: bufferRef, + buffer: (*bufferRef)[:0], } }, } @@ -43,7 +46,7 @@ func NewScanner(r io.Reader) *Scanner { s.reader = bufio.NewReader(r) s.line = 1 s.col = 0 - s.buffer = s.buffer[:0] + s.buffer = (*s.bufferRef)[:0] s.token = Token{Type: TokenError} return s } @@ -53,7 +56,7 @@ func ReleaseScanner(s *Scanner) { if s != nil { // Clear references but keep allocated memory s.reader = nil - s.buffer = s.buffer[:0] + s.buffer = (*s.bufferRef)[:0] scannerPool.Put(s) } } @@ -139,19 +142,19 @@ func (s *Scanner) NextToken() (Token, error) { // Skip whitespace err := s.SkipWhitespace() - if err == io.EOF { - return Token{Type: TokenEOF}, nil - } if err != nil { - return Token{Type: TokenError, Value: []byte(err.Error())}, err + 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 } b, err := s.PeekByte() if err != nil { if err == io.EOF { - return Token{Type: TokenEOF}, nil + return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil } - return Token{Type: TokenError, Value: []byte(err.Error())}, err + return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err } // Record start position for error reporting @@ -159,10 +162,6 @@ func (s *Scanner) NextToken() (Token, error) { // Process based on first character switch { - case b == '=': - _, _ = s.ReadByte() // consume equals - return Token{Type: TokenEquals, Line: startLine, Column: startColumn}, nil - case b == '{': _, _ = s.ReadByte() // consume open brace return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil @@ -264,7 +263,7 @@ func (s *Scanner) scanComment() error { // scanString scans a quoted string func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { // Reset buffer - s.buffer = s.buffer[:0] + s.buffer = (*s.bufferRef)[:0] // Consume opening quote _, err := s.ReadByte() @@ -317,7 +316,7 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { // scanName scans an identifier func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { // Reset buffer - s.buffer = s.buffer[:0] + s.buffer = (*s.bufferRef)[:0] // Read first character b, err := s.ReadByte() @@ -363,7 +362,7 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { // scanNumber scans a numeric value func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { // Reset buffer - s.buffer = s.buffer[:0] + s.buffer = (*s.bufferRef)[:0] // Read first character (might be a minus sign or digit) b, err := s.ReadByte() diff --git a/tests/config_test.go b/tests/config_test.go index 3d5be5f..2c3571a 100644 --- a/tests/config_test.go +++ b/tests/config_test.go @@ -10,13 +10,13 @@ import ( func TestBasicKeyValuePairs(t *testing.T) { input := ` - boolTrue = true - boolFalse = false - integer = 42 - negativeInt = -10 - floatValue = 3.14 - negativeFloat = -2.5 - stringValue = "hello world" + boolTrue true + boolFalse false + integer 42 + negativeInt -10 + floatValue 3.14 + negativeFloat -2.5 + stringValue "hello world" ` config, err := config.Load(strings.NewReader(input)) if err != nil { @@ -83,18 +83,18 @@ func TestBasicKeyValuePairs(t *testing.T) { func TestComments(t *testing.T) { input := ` -- This is a line comment - key1 = "value1" + key1 "value1" --[[ This is a block comment spanning multiple lines ]] - key2 = "value2" + key2 "value2" settings { -- Comment inside a map - timeout = 30 + timeout 30 --[[ Another block comment ]] - retries = 3 + retries 3 } ` @@ -135,15 +135,6 @@ func TestArrays(t *testing.T) { "cherry" } - -- Array with equals sign - numbers = { - 1 - 2 - 3 - 4 - 5 - } - -- Mixed types array mixed { "string" @@ -175,23 +166,6 @@ func TestArrays(t *testing.T) { t.Errorf("expected fruits.0=\"apple\", got %v, err: %v", apple, err) } - // Verify array with equals sign - numbers, err := config.GetArray("numbers") - if err != nil { - t.Fatalf("failed to get numbers array: %v", err) - } - - // Check array length - if len(numbers) != 5 { - t.Errorf("expected 5 numbers, got %d", len(numbers)) - } - - // Verify first number - firstNumber, err := config.GetInt("numbers.0") - if err != nil || firstNumber != 1 { - t.Errorf("expected numbers.0=1, got %v, err: %v", firstNumber, err) - } - // Verify mixed types array mixed, err := config.GetArray("mixed") if err != nil { @@ -228,28 +202,20 @@ func TestMaps(t *testing.T) { input := ` -- Simple map server { - host = "localhost" - port = 8080 - } - - -- Map with equals sign - database = { - username = "admin" - password = "secret" - enabled = true - maxConnections = 100 + host "localhost" + port 8080 } -- Nested maps application { - name = "MyApp" - version = "1.0.0" + name "MyApp" + version "1.0.0" settings { - theme = "dark" - notifications = true + theme "dark" + notifications true logging { - level = "info" - file = "app.log" + level "info" + file "app.log" } } } @@ -281,17 +247,6 @@ func TestMaps(t *testing.T) { t.Errorf("expected server.port=8080, got %v, err: %v", port, err) } - // Verify map with equals sign - dbUser, err := config.GetString("database.username") - if err != nil || dbUser != "admin" { - t.Errorf("expected database.username=\"admin\", got %v, err: %v", dbUser, err) - } - - dbEnabled, err := config.GetBool("database.enabled") - if err != nil || dbEnabled != true { - t.Errorf("expected database.enabled=true, got %v, err: %v", dbEnabled, err) - } - // Verify deeply nested maps appName, err := config.GetString("application.name") if err != nil || appName != "MyApp" { diff --git a/token.go b/token.go index 9170548..176fd0e 100644 --- a/token.go +++ b/token.go @@ -10,7 +10,6 @@ const ( TokenString TokenNumber TokenBoolean - TokenEquals TokenOpenBrace TokenCloseBrace TokenComment