v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Enforce 64-bit platform; fails compilation on 32-bit targets where int(int64) in parseInteger would truncate
|
||||
const _ uint = 1<<63 - 1
|
||||
|
||||
// Parser parses TOML tokens into a map[string]any
|
||||
type Parser struct {
|
||||
root map[string]any
|
||||
lexer *Lexer
|
||||
current any // Pointer to the current map or slice of maps being populated (scope)
|
||||
// Identity set of inline-table maps (immutable per TOML spec)
|
||||
frozen map[uintptr]bool
|
||||
curToken Token
|
||||
peekToken Token
|
||||
// Value nesting depth (arrays / inline tables)
|
||||
depth int
|
||||
}
|
||||
|
||||
// Recursion bound for parseValue -> parseArray/parseInlineTable
|
||||
const maxValueDepth = 1000
|
||||
|
||||
func NewParser(input []byte) *Parser {
|
||||
l := NewLexer(input)
|
||||
p := &Parser{
|
||||
lexer: l,
|
||||
root: make(map[string]any),
|
||||
frozen: make(map[uintptr]bool),
|
||||
}
|
||||
p.nextToken()
|
||||
p.nextToken()
|
||||
p.current = p.root
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Parser) nextToken() {
|
||||
p.curToken = p.peekToken
|
||||
p.peekToken = p.lexer.NextToken()
|
||||
|
||||
// Skip comments automatically
|
||||
for p.peekToken.Type == TokenComment {
|
||||
p.peekToken = p.lexer.NextToken()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) Parse() (map[string]any, error) {
|
||||
for p.curToken.Type != TokenEOF {
|
||||
if p.curToken.Type == TokenNewline {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.parseStatement(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return p.root, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseStatement() error {
|
||||
switch p.curToken.Type {
|
||||
case TokenLBracket:
|
||||
// Table Definition: [table] or [[array.table]]
|
||||
return p.parseTableDeclaration()
|
||||
case TokenIdent, TokenString:
|
||||
// Key-Value Pair: key = value
|
||||
return p.parseKeyValuePair(p.current)
|
||||
case TokenError:
|
||||
return fmt.Errorf("lexing error line %d: %s", p.curToken.Line, p.curToken.Literal)
|
||||
default:
|
||||
return fmt.Errorf("unexpected token line %d: %s", p.curToken.Line, p.curToken.String())
|
||||
}
|
||||
}
|
||||
|
||||
// parseTableDeclaration handles [key] and [[key]]
|
||||
func (p *Parser) parseTableDeclaration() error {
|
||||
isArray := false
|
||||
if p.peekToken.Type == TokenLBracket {
|
||||
// It is [[ ...
|
||||
p.nextToken() // consume first [
|
||||
isArray = true
|
||||
}
|
||||
p.nextToken() // consume [
|
||||
|
||||
// Parse Key (dotted)
|
||||
keys, err := p.parseKeyParts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isArray {
|
||||
if p.curToken.Type != TokenRBracket {
|
||||
return fmt.Errorf("expected closing bracket for array table at line %d", p.curToken.Line)
|
||||
}
|
||||
p.nextToken() // consume first ]
|
||||
}
|
||||
|
||||
if p.curToken.Type != TokenRBracket {
|
||||
return fmt.Errorf("expected closing bracket for table at line %d", p.curToken.Line)
|
||||
}
|
||||
p.nextToken() // consume final ]
|
||||
|
||||
// Define scope
|
||||
return p.setTableScope(keys, isArray)
|
||||
}
|
||||
|
||||
// setTableScope navigates/creates the map structure and sets p.current
|
||||
func (p *Parser) setTableScope(keys []string, isArrayOfTables bool) error {
|
||||
// Table declarations always start from root
|
||||
var ptr any = p.root
|
||||
|
||||
for i, key := range keys {
|
||||
isLast := i == len(keys)-1
|
||||
currentMap, ok := ptr.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("key path conflict: %s is not a map", key)
|
||||
}
|
||||
|
||||
if isLast {
|
||||
if isArrayOfTables {
|
||||
// [[a.b]] -> Ensure 'b' is a slice of maps, append new map, set cursor to it
|
||||
var slice []map[string]any
|
||||
if val, exists := currentMap[key]; exists {
|
||||
if s, ok := val.([]map[string]any); ok {
|
||||
slice = s
|
||||
} else {
|
||||
return fmt.Errorf("key conflict: %s is not an array of tables", key)
|
||||
}
|
||||
} else {
|
||||
slice = make([]map[string]any, 0)
|
||||
}
|
||||
|
||||
newMap := make(map[string]any)
|
||||
slice = append(slice, newMap)
|
||||
currentMap[key] = slice
|
||||
p.current = newMap
|
||||
} else {
|
||||
// [a.b] -> Ensure 'b' is a map, set cursor to it
|
||||
var targetMap map[string]any
|
||||
if val, exists := currentMap[key]; exists {
|
||||
if m, ok := val.(map[string]any); ok {
|
||||
// Inline tables cannot be reopened
|
||||
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
||||
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
||||
}
|
||||
targetMap = m
|
||||
} else {
|
||||
return fmt.Errorf("key conflict: %s is not a table", key)
|
||||
}
|
||||
} else {
|
||||
targetMap = make(map[string]any)
|
||||
currentMap[key] = targetMap
|
||||
}
|
||||
p.current = targetMap
|
||||
}
|
||||
} else {
|
||||
// Intermediate key -> ensure map exists and traverse.
|
||||
// Traversal through an existing [[array]] descends into its last element.
|
||||
if val, exists := currentMap[key]; exists {
|
||||
if m, ok := val.(map[string]any); ok {
|
||||
// Inline tables cannot be extended via sub-tables
|
||||
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
||||
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
||||
}
|
||||
ptr = m
|
||||
} else if slice, ok := val.([]map[string]any); ok {
|
||||
if len(slice) == 0 {
|
||||
return fmt.Errorf("cannot traverse empty array table %s", key)
|
||||
}
|
||||
ptr = slice[len(slice)-1]
|
||||
} else {
|
||||
return fmt.Errorf("intermediate key %s is not a map", key)
|
||||
}
|
||||
} else {
|
||||
newMap := make(map[string]any)
|
||||
currentMap[key] = newMap
|
||||
ptr = newMap
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseKeyValuePair(scope any) error {
|
||||
// Parse Key (dotted allowed: a.b.c = 1)
|
||||
keys, err := p.parseKeyParts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.curToken.Type != TokenEqual {
|
||||
return fmt.Errorf("expected '=' after key at line %d, got %s", p.curToken.Line, p.curToken.String())
|
||||
}
|
||||
p.nextToken() // consume =
|
||||
|
||||
val, err := p.parseValue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assign value to scope
|
||||
return p.assignValue(scope, keys, val)
|
||||
}
|
||||
|
||||
func (p *Parser) assignValue(scope any, keys []string, val any) error {
|
||||
ptr := scope
|
||||
|
||||
// If scope is map, easy. If scope is not map, error.
|
||||
currentMap, ok := ptr.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("scope is not a map")
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
if i == len(keys)-1 {
|
||||
// Final key, assign value
|
||||
if _, exists := currentMap[key]; exists {
|
||||
return fmt.Errorf("duplicate key %s at line %d", key, p.curToken.Line)
|
||||
}
|
||||
currentMap[key] = val
|
||||
} else {
|
||||
// Intermediate, ensure map
|
||||
if existing, exists := currentMap[key]; exists {
|
||||
if m, ok := existing.(map[string]any); ok {
|
||||
if p.frozen[reflect.ValueOf(m).Pointer()] {
|
||||
return fmt.Errorf("cannot extend inline table %q at line %d", key, p.curToken.Line)
|
||||
}
|
||||
currentMap = m
|
||||
} else {
|
||||
return fmt.Errorf("intermediate key %s is not a map", key)
|
||||
}
|
||||
} else {
|
||||
newMap := make(map[string]any)
|
||||
currentMap[key] = newMap
|
||||
currentMap = newMap
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseKeyParts() ([]string, error) {
|
||||
var keys []string
|
||||
for {
|
||||
// Rule: Tokens identified as Numbers are forbidden as keys
|
||||
if p.curToken.Type == TokenInteger || p.curToken.Type == TokenFloat {
|
||||
return nil, fmt.Errorf("numeric keys are forbidden: %q", p.curToken.Literal)
|
||||
}
|
||||
|
||||
if p.curToken.Type == TokenString {
|
||||
// Rule: Even quoted strings shouldn't be pure numbers per instruction
|
||||
if _, err := strconv.Atoi(p.curToken.Literal); err == nil {
|
||||
return nil, fmt.Errorf("numeric string keys are forbidden: %q", p.curToken.Literal)
|
||||
}
|
||||
}
|
||||
|
||||
if p.curToken.Type != TokenIdent && p.curToken.Type != TokenString {
|
||||
return nil, fmt.Errorf("expected key, got %s", p.curToken.String())
|
||||
}
|
||||
|
||||
keys = append(keys, p.curToken.Literal)
|
||||
p.nextToken()
|
||||
|
||||
if p.curToken.Type == TokenDot {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseValue() (any, error) {
|
||||
// Guard unbounded recursion on nested
|
||||
p.depth++
|
||||
defer func() { p.depth-- }()
|
||||
if p.depth > maxValueDepth {
|
||||
return nil, fmt.Errorf("value nesting exceeds %d at line %d", maxValueDepth, p.curToken.Line)
|
||||
}
|
||||
|
||||
switch p.curToken.Type {
|
||||
case TokenString:
|
||||
val := p.curToken.Literal
|
||||
p.nextToken()
|
||||
return val, nil
|
||||
case TokenInteger:
|
||||
val, err := p.parseInteger(p.curToken.Literal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid integer %q at line %d: %w", p.curToken.Literal, p.curToken.Line, err)
|
||||
}
|
||||
p.nextToken()
|
||||
return val, nil
|
||||
case TokenFloat:
|
||||
val, err := strconv.ParseFloat(p.curToken.Literal, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid float %q at line %d: %w", p.curToken.Literal, p.curToken.Line, err)
|
||||
}
|
||||
p.nextToken()
|
||||
return val, nil
|
||||
case TokenBool:
|
||||
val := p.curToken.Literal == "true"
|
||||
p.nextToken()
|
||||
return val, nil
|
||||
case TokenLBracket:
|
||||
return p.parseArray()
|
||||
case TokenLBrace:
|
||||
return p.parseInlineTable()
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected value token %s at line %d", p.curToken.String(), p.curToken.Line)
|
||||
}
|
||||
|
||||
func (p *Parser) parseInteger(lit string) (int, error) {
|
||||
// Handle optional leading sign
|
||||
negative := false
|
||||
numLit := lit
|
||||
if len(numLit) > 0 && (numLit[0] == '+' || numLit[0] == '-') {
|
||||
negative = numLit[0] == '-'
|
||||
numLit = numLit[1:]
|
||||
}
|
||||
|
||||
var val int64
|
||||
var err error
|
||||
|
||||
if len(numLit) > 2 && numLit[0] == '0' {
|
||||
switch numLit[1] {
|
||||
case 'x', 'X':
|
||||
val, err = strconv.ParseInt(numLit[2:], 16, 64)
|
||||
case 'o', 'O':
|
||||
val, err = strconv.ParseInt(numLit[2:], 8, 64)
|
||||
case 'b', 'B':
|
||||
val, err = strconv.ParseInt(numLit[2:], 2, 64)
|
||||
default:
|
||||
val, err = strconv.ParseInt(lit, 10, 64)
|
||||
return int(val), err
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if negative {
|
||||
val = -val
|
||||
}
|
||||
return int(val), nil
|
||||
}
|
||||
|
||||
val, err = strconv.ParseInt(lit, 10, 64)
|
||||
return int(val), err
|
||||
}
|
||||
|
||||
func (p *Parser) parseArray() ([]any, error) {
|
||||
p.nextToken() // consume [
|
||||
arr := make([]any, 0)
|
||||
|
||||
for p.curToken.Type != TokenRBracket {
|
||||
if p.curToken.Type == TokenNewline {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
|
||||
if p.curToken.Type == TokenComma {
|
||||
p.nextToken()
|
||||
} else if p.curToken.Type != TokenRBracket {
|
||||
// Check for newlines between elements if missing comma? TOML usually requires comma.
|
||||
// Relaxed parser: require comma unless followed immediately by bracket (trailing comma allowed)
|
||||
if p.curToken.Type == TokenNewline {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("expected comma or closing bracket in array at line %d", p.curToken.Line)
|
||||
}
|
||||
}
|
||||
p.nextToken() // consume ]
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseInlineTable() (map[string]any, error) {
|
||||
p.nextToken() // consume {
|
||||
m := make(map[string]any)
|
||||
|
||||
for p.curToken.Type != TokenRBrace {
|
||||
if p.curToken.Type == TokenNewline {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse key = value
|
||||
keys, err := p.parseKeyParts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.curToken.Type != TokenEqual {
|
||||
return nil, fmt.Errorf("expected '=' in inline table at line %d", p.curToken.Line)
|
||||
}
|
||||
p.nextToken()
|
||||
|
||||
val, err := p.parseValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Inline tables can have dotted keys too: { a.b = 1 }
|
||||
if err := p.assignValue(m, keys, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.curToken.Type == TokenComma {
|
||||
p.nextToken()
|
||||
} else if p.curToken.Type != TokenRBrace {
|
||||
if p.curToken.Type == TokenNewline {
|
||||
p.nextToken()
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("expected comma or closing brace in inline table at line %d", p.curToken.Line)
|
||||
}
|
||||
}
|
||||
p.nextToken() // consume }
|
||||
|
||||
// Mark inline table immutable. Value.Pointer for maps is documented
|
||||
// stable for identity comparison. Nested inline tables self-mark on return;
|
||||
// same-table dotted assignments happen before the mark, so intra-table
|
||||
// dotted keys ({a.b = 1}) remain unaffected.
|
||||
p.frozen[reflect.ValueOf(m).Pointer()] = true
|
||||
return m, nil
|
||||
}
|
||||
Reference in New Issue
Block a user