This commit is contained in:
Sky Johnson 2025-03-04 17:29:39 -06:00
parent 44827e16d4
commit 456854246b
6 changed files with 425 additions and 423 deletions

View File

@ -15,10 +15,10 @@ import (
func BenchmarkSmallConfig(b *testing.B) { func BenchmarkSmallConfig(b *testing.B) {
// Small config with just a few key-value pairs // Small config with just a few key-value pairs
smallConfig := ` smallConfig := `
host = "localhost" host "localhost"
port = 8080 port 8080
debug = true debug true
timeout = 30 timeout 30
` `
b.ResetTimer() b.ResetTimer()
@ -35,21 +35,21 @@ func BenchmarkMediumConfig(b *testing.B) {
// Medium config with nested structures and arrays // Medium config with nested structures and arrays
mediumConfig := ` mediumConfig := `
app { app {
name = "TestApp" name "TestApp"
version = "1.0.0" version "1.0.0"
enableLogging = true enableLogging true
} }
database { database {
host = "db.example.com" host "db.example.com"
port = 5432 port 5432
credentials { credentials {
username = "admin" username "admin"
password = "secure123" password "secure123"
} }
} }
features = { features {
"authentication" "authentication"
"authorization" "authorization"
"reporting" "reporting"
@ -57,18 +57,18 @@ func BenchmarkMediumConfig(b *testing.B) {
} }
timeouts { timeouts {
connect = 5 connect 5
read = 10 read 10
write = 10 write 10
idle = 60 idle 60
} }
-- Comments to add some parsing overhead -- Comments to add some parsing overhead
endpoints { endpoints {
api = "/api/v1" api "/api/v1"
web = "/web" web "/web"
admin = "/admin" admin "/admin"
health = "/health" health "/health"
} }
` `
@ -86,38 +86,38 @@ func BenchmarkLargeConfig(b *testing.B) {
// Simpler large config with careful bracket matching // Simpler large config with careful bracket matching
largeConfig := ` largeConfig := `
application { application {
name = "EnterpriseApp" name "EnterpriseApp"
version = "2.5.1" version "2.5.1"
environment = "production" environment "production"
debug = false debug false
maxConnections = 1000 maxConnections 1000
timeout = 30 timeout 30
retryCount = 3 retryCount 3
logLevel = "info" logLevel "info"
} }
-- Database cluster configuration -- Database cluster configuration
databases { databases {
primary { primary {
host = "primary-db.example.com" host "primary-db.example.com"
port = 5432 port 5432
maxConnections = 100 maxConnections 100
credentials { credentials {
username = "app_user" username "app_user"
password = "super_secret" password "super_secret"
ssl = true ssl true
timeout = 5 timeout 5
} }
} }
replica { replica {
host = "replica-db.example.com" host "replica-db.example.com"
port = 5432 port 5432
maxConnections = 200 maxConnections 200
credentials { credentials {
username = "read_user" username "read_user"
password = "read_only_pw" password "read_only_pw"
ssl = true ssl true
} }
} }
} }
@ -140,7 +140,7 @@ func BenchmarkLargeConfig(b *testing.B) {
for i := 1; i <= 50; i++ { for i := 1; i <= 50; i++ {
builder.WriteString("\n\t\tsetting") builder.WriteString("\n\t\tsetting")
builder.WriteString(strconv.Itoa(i)) builder.WriteString(strconv.Itoa(i))
builder.WriteString(" = ") builder.WriteString(" ")
builder.WriteString(strconv.Itoa(i * 10)) builder.WriteString(strconv.Itoa(i * 10))
} }
@ -150,7 +150,7 @@ func BenchmarkLargeConfig(b *testing.B) {
roles { roles {
admin { admin {
permissions = { permissions {
"read" "read"
"write" "write"
"delete" "delete"
@ -158,7 +158,7 @@ func BenchmarkLargeConfig(b *testing.B) {
} }
} }
user { user {
permissions = { permissions {
"read" "read"
"write" "write"
} }
@ -619,10 +619,10 @@ permissions = ["read", "write"]
// Value Retrieval Benchmarks for Custom Config Format // Value Retrieval Benchmarks for Custom Config Format
func BenchmarkRetrieveSimpleValues(b *testing.B) { func BenchmarkRetrieveSimpleValues(b *testing.B) {
configData := ` configData := `
host = "localhost" host "localhost"
port = 8080 port 8080
debug = true debug true
timeout = 30 timeout 30
` `
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
@ -663,19 +663,19 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) {
func BenchmarkRetrieveNestedValues(b *testing.B) { func BenchmarkRetrieveNestedValues(b *testing.B) {
configData := ` configData := `
app { app {
name = "TestApp" name "TestApp"
version = "1.0.0" version "1.0.0"
settings { settings {
enableLogging = true enableLogging true
maxConnections = 100 maxConnections 100
} }
} }
database { database {
host = "db.example.com" host "db.example.com"
port = 5432 port 5432
credentials { credentials {
username = "admin" username "admin"
password = "secure123" password "secure123"
} }
} }
` `
@ -715,13 +715,13 @@ func BenchmarkRetrieveNestedValues(b *testing.B) {
func BenchmarkRetrieveArrayValues(b *testing.B) { func BenchmarkRetrieveArrayValues(b *testing.B) {
configData := ` configData := `
features = { features {
"authentication" "authentication"
"authorization" "authorization"
"reporting" "reporting"
"analytics" "analytics"
} }
numbers = { numbers {
1 1
2 2
3 3
@ -767,19 +767,19 @@ func BenchmarkRetrieveArrayValues(b *testing.B) {
func BenchmarkRetrieveMixedValues(b *testing.B) { func BenchmarkRetrieveMixedValues(b *testing.B) {
configData := ` configData := `
app { app {
name = "TestApp" name "TestApp"
version = "1.0.0" version "1.0.0"
environments = { environments {
"development" "development"
"testing" "testing"
"production" "production"
} }
limits { limits {
requests = 1000 requests 1000
connections = 100 connections 100
timeouts { timeouts {
read = 5 read 5
write = 10 write 10
} }
} }
} }
@ -1065,17 +1065,17 @@ func BenchmarkComplexValueRetrieval(b *testing.B) {
// Setup complex config with deep nesting and arrays for all formats // Setup complex config with deep nesting and arrays for all formats
customConfig := ` customConfig := `
app { app {
name = "TestApp" name "TestApp"
version = "1.0.0" version "1.0.0"
settings { settings {
enableLogging = true enableLogging true
maxConnections = 100 maxConnections 100
timeouts { timeouts {
read = 5 read 5
write = 10 write 10
} }
} }
environments = { environments {
"development" "development"
"testing" "testing"
"production" "production"

504
config.go
View File

@ -6,18 +6,10 @@ import (
"strconv" "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 // Config holds a single hierarchical structure like JSON and handles parsing
type Config struct { type Config struct {
data map[string]any data map[string]any
dataRef *map[string]any // Reference to pooled map
scanner *Scanner scanner *Scanner
currentObject map[string]any currentObject map[string]any
stack []map[string]any stack []map[string]any
@ -26,14 +18,34 @@ type Config struct {
// NewConfig creates a new empty config // NewConfig creates a new empty config
func NewConfig() *Config { func NewConfig() *Config {
dataRef := GetMap()
data := *dataRef
cfg := &Config{ cfg := &Config{
data: make(map[string]any, 16), // Pre-allocate with expected capacity data: data,
stack: make([]map[string]any, 0, 8), dataRef: dataRef,
stack: make([]map[string]any, 0, 8),
} }
cfg.currentObject = cfg.data cfg.currentObject = cfg.data
return cfg 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 // Get retrieves a value from the config using dot notation
func (c *Config) Get(key string) (any, error) { func (c *Config) Get(key string) (any, error) {
if key == "" { if key == "" {
@ -56,7 +68,6 @@ func (c *Config) Get(key string) (any, error) {
// Handle current node based on its type // Handle current node based on its type
switch node := current.(type) { switch node := current.(type) {
case map[string]any: case map[string]any:
// Simple map lookup
val, ok := node[part] val, ok := node[part]
if !ok { if !ok {
return nil, fmt.Errorf("key %s not found", part) return nil, fmt.Errorf("key %s not found", part)
@ -64,7 +75,6 @@ func (c *Config) Get(key string) (any, error) {
current = val current = val
case []any: case []any:
// Must be numeric index
index, err := strconv.Atoi(part) index, err := strconv.Atoi(part)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid array index: %s", part) 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) 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 || (i < len(key)-1 && key[i] == '.' && end == i) {
if i == len(key)-1 { if i == len(key)-1 {
return current, nil 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) 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 // Error creates an error with line information from the current token
func (c *Config) Error(msg string) error { func (c *Config) Error(msg string) error {
return fmt.Errorf("line %d, column %d: %s", 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 // We expect top level entries to be names
if token.Type != TokenName { 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 // Get the property name - copy to create a stable key
nameBytes := token.Value nameBytes := GetByteSlice()
name := string(nameBytes) *nameBytes = append((*nameBytes)[:0], token.Value...)
name := string(*nameBytes)
PutByteSlice(nameBytes)
// Get the next token (should be = or {) // Get the next token
token, err = c.nextToken() nextToken, err := c.nextToken()
if err != nil { if err != nil {
if err == io.EOF {
// EOF after name - store as empty string
c.currentObject[name] = ""
break
}
return err return err
} }
var value any var value any
if token.Type == TokenEquals { if nextToken.Type == TokenOpenBrace {
// It's a standard key=value assignment // It's a nested object/array
value, err = c.parseValue()
if err != nil {
return err
}
} else if token.Type == TokenOpenBrace {
// It's a map/array without '='
value, err = c.parseObject() value, err = c.parseObject()
if err != nil { if err != nil {
return err return err
} }
} else { } 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 // Store the value in the config
if mapValue, ok := value.(map[string]any); ok { c.currentObject[name] = value
// 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
}
} }
return nil 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 // parseObject parses a map or array
func (c *Config) parseObject() (any, error) { func (c *Config) parseObject() (any, error) {
// Initialize stack with first state // Default to treating as an array until we see a name
stack := []*ParseState{{ isArray := true
object: make(map[string]any, 8), arrayRef := GetArray()
arrayElements: make([]any, 0, 8), arrayElements := *arrayRef
isArray: true,
}}
for len(stack) > 0 { mapRef := GetMap()
// Get current state from top of stack objectElements := *mapRef
current := stack[len(stack)-1]
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() token, err := c.nextToken()
if err != nil { if err != nil {
if err == io.EOF {
return nil, fmt.Errorf("unexpected EOF in object/array")
}
return nil, err return nil, err
} }
// Handle closing brace - finish current object/array // Handle closing brace - finish current object/array
if token.Type == TokenCloseBrace { if token.Type == TokenCloseBrace {
// Determine result based on what we've collected if isArray && len(arrayElements) > 0 {
var result any result := arrayElements
if current.isArray && len(current.object) == 0 { // Don't release the array, transfer ownership
result = current.arrayElements *arrayRef = nil // Detach from pool reference
} 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 {
return result, nil return result, nil
} }
result := objectElements
// Otherwise, add result to parent // Don't release the map, transfer ownership
parent := stack[len(stack)-1] *mapRef = nil // Detach from pool reference
if parent.expectValue { return result, nil
parent.object[parent.currentKey] = result
parent.expectValue = false
} else {
parent.arrayElements = append(parent.arrayElements, result)
}
continue
} }
// Handle tokens based on type // Handle tokens based on type
switch token.Type { switch token.Type {
case TokenName: 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() 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 { if err != nil {
return nil, err return nil, err
} }
if nextToken.Type == TokenEquals { if isArray {
// Key-value pair arrayElements = append(arrayElements, nestedValue)
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)
} else { } else {
// Array element // If we're in an object context, this is an error
c.scanner.UnreadToken(nextToken) return nil, c.Error("unexpected nested object without a key")
// Convert name to appropriate type
value := c.convertNameValue(name)
current.arrayElements = append(current.arrayElements, value)
} }
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: 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 // Load parses a config from a reader
@ -503,13 +464,80 @@ func Load(r io.Reader) (*Config, error) {
err := config.Parse(r) err := config.Parse(r)
if err != nil { if err != nil {
config.Release()
return nil, err return nil, err
} }
return config, nil 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 { func isLetter(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
@ -519,77 +547,27 @@ func isDigit(b byte) bool {
return b >= '0' && b <= '9' return b >= '0' && b <= '9'
} }
// ParseNumber converts a string to a number (int64 or float64) func isDigitOrMinus(b byte) bool {
func ParseNumber(s string) (any, error) { return isDigit(b) || b == '-'
// 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)
} }
// bytesEqual compares a byte slice with either a string or byte slice func bytesEqual(b1, b2 []byte) bool {
func bytesEqual(b []byte, s []byte) bool { if len(b1) != len(b2) {
if len(b) != len(s) {
return false return false
} }
for i := 0; i < len(b); i++ { for i := 0; i < len(b1); i++ {
if b[i] != s[i] { if b1[i] != b2[i] {
return false return false
} }
} }
return true return true
} }
// isDigitOrMinus checks if a string starts with a digit or minus sign func containsChar(b []byte, c byte) bool {
func isDigitOrMinus(s string) bool { for _, v := range b {
if len(s) == 0 { if v == c {
return false return true
}
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)
} }
} }
// It's an integer return false
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
} }

71
pool.go Normal file
View File

@ -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)
}
}

View File

@ -19,20 +19,23 @@ var (
// Scanner handles the low-level parsing of the configuration format // Scanner handles the low-level parsing of the configuration format
type Scanner struct { type Scanner struct {
reader *bufio.Reader reader *bufio.Reader
line int line int
col int col int
buffer []byte buffer []byte // Slice to the pooled buffer
token Token // Current token for unread bufferRef *[]byte // Reference to the pooled buffer
token Token // Current token for unread
} }
// scannerPool helps reuse scanner objects // scannerPool helps reuse scanner objects
var scannerPool = sync.Pool{ var scannerPool = sync.Pool{
New: func() interface{} { New: func() interface{} {
bufferRef := GetByteSlice()
return &Scanner{ return &Scanner{
line: 1, line: 1,
col: 0, col: 0,
buffer: make([]byte, 0, 128), bufferRef: bufferRef,
buffer: (*bufferRef)[:0],
} }
}, },
} }
@ -43,7 +46,7 @@ func NewScanner(r io.Reader) *Scanner {
s.reader = bufio.NewReader(r) s.reader = bufio.NewReader(r)
s.line = 1 s.line = 1
s.col = 0 s.col = 0
s.buffer = s.buffer[:0] s.buffer = (*s.bufferRef)[:0]
s.token = Token{Type: TokenError} s.token = Token{Type: TokenError}
return s return s
} }
@ -53,7 +56,7 @@ func ReleaseScanner(s *Scanner) {
if s != nil { if s != nil {
// Clear references but keep allocated memory // Clear references but keep allocated memory
s.reader = nil s.reader = nil
s.buffer = s.buffer[:0] s.buffer = (*s.bufferRef)[:0]
scannerPool.Put(s) scannerPool.Put(s)
} }
} }
@ -139,19 +142,19 @@ func (s *Scanner) NextToken() (Token, error) {
// Skip whitespace // Skip whitespace
err := s.SkipWhitespace() err := s.SkipWhitespace()
if err == io.EOF {
return Token{Type: TokenEOF}, nil
}
if err != 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() b, err := s.PeekByte()
if err != nil { if err != nil {
if err == io.EOF { 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 // Record start position for error reporting
@ -159,10 +162,6 @@ func (s *Scanner) NextToken() (Token, error) {
// Process based on first character // Process based on first character
switch { switch {
case b == '=':
_, _ = s.ReadByte() // consume equals
return Token{Type: TokenEquals, Line: startLine, Column: startColumn}, nil
case b == '{': case b == '{':
_, _ = s.ReadByte() // consume open brace _, _ = s.ReadByte() // consume open brace
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
@ -264,7 +263,7 @@ func (s *Scanner) scanComment() error {
// scanString scans a quoted string // scanString scans a quoted string
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
// Reset buffer // Reset buffer
s.buffer = s.buffer[:0] s.buffer = (*s.bufferRef)[:0]
// Consume opening quote // Consume opening quote
_, err := s.ReadByte() _, err := s.ReadByte()
@ -317,7 +316,7 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
// scanName scans an identifier // scanName scans an identifier
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
// Reset buffer // Reset buffer
s.buffer = s.buffer[:0] s.buffer = (*s.bufferRef)[:0]
// Read first character // Read first character
b, err := s.ReadByte() b, err := s.ReadByte()
@ -363,7 +362,7 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
// scanNumber scans a numeric value // scanNumber scans a numeric value
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
// Reset buffer // Reset buffer
s.buffer = s.buffer[:0] s.buffer = (*s.bufferRef)[:0]
// Read first character (might be a minus sign or digit) // Read first character (might be a minus sign or digit)
b, err := s.ReadByte() b, err := s.ReadByte()

View File

@ -10,13 +10,13 @@ import (
func TestBasicKeyValuePairs(t *testing.T) { func TestBasicKeyValuePairs(t *testing.T) {
input := ` input := `
boolTrue = true boolTrue true
boolFalse = false boolFalse false
integer = 42 integer 42
negativeInt = -10 negativeInt -10
floatValue = 3.14 floatValue 3.14
negativeFloat = -2.5 negativeFloat -2.5
stringValue = "hello world" stringValue "hello world"
` `
config, err := config.Load(strings.NewReader(input)) config, err := config.Load(strings.NewReader(input))
if err != nil { if err != nil {
@ -83,18 +83,18 @@ func TestBasicKeyValuePairs(t *testing.T) {
func TestComments(t *testing.T) { func TestComments(t *testing.T) {
input := ` input := `
-- This is a line comment -- This is a line comment
key1 = "value1" key1 "value1"
--[[ This is a --[[ This is a
block comment spanning block comment spanning
multiple lines ]] multiple lines ]]
key2 = "value2" key2 "value2"
settings { settings {
-- Comment inside a map -- Comment inside a map
timeout = 30 timeout 30
--[[ Another block comment ]] --[[ Another block comment ]]
retries = 3 retries 3
} }
` `
@ -135,15 +135,6 @@ func TestArrays(t *testing.T) {
"cherry" "cherry"
} }
-- Array with equals sign
numbers = {
1
2
3
4
5
}
-- Mixed types array -- Mixed types array
mixed { mixed {
"string" "string"
@ -175,23 +166,6 @@ func TestArrays(t *testing.T) {
t.Errorf("expected fruits.0=\"apple\", got %v, err: %v", apple, err) 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 // Verify mixed types array
mixed, err := config.GetArray("mixed") mixed, err := config.GetArray("mixed")
if err != nil { if err != nil {
@ -228,28 +202,20 @@ func TestMaps(t *testing.T) {
input := ` input := `
-- Simple map -- Simple map
server { server {
host = "localhost" host "localhost"
port = 8080 port 8080
}
-- Map with equals sign
database = {
username = "admin"
password = "secret"
enabled = true
maxConnections = 100
} }
-- Nested maps -- Nested maps
application { application {
name = "MyApp" name "MyApp"
version = "1.0.0" version "1.0.0"
settings { settings {
theme = "dark" theme "dark"
notifications = true notifications true
logging { logging {
level = "info" level "info"
file = "app.log" file "app.log"
} }
} }
} }
@ -281,17 +247,6 @@ func TestMaps(t *testing.T) {
t.Errorf("expected server.port=8080, got %v, err: %v", port, err) 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 // Verify deeply nested maps
appName, err := config.GetString("application.name") appName, err := config.GetString("application.name")
if err != nil || appName != "MyApp" { if err != nil || appName != "MyApp" {

View File

@ -10,7 +10,6 @@ const (
TokenString TokenString
TokenNumber TokenNumber
TokenBoolean TokenBoolean
TokenEquals
TokenOpenBrace TokenOpenBrace
TokenCloseBrace TokenCloseBrace
TokenComment TokenComment