Files

893 lines
19 KiB
Go

package interpres
import (
"fmt"
"strconv"
"strings"
)
// parser is a recursive-descent TOML parser producing a map[string]any tree.
type parser struct {
src []rune
pos int
line int
root map[string]any
current map[string]any
headers map[string]bool
frozen map[string]bool
dotted map[string]bool
arrays map[string]bool
currentPath []string
}
func (p *parser) parse() (map[string]any, error) {
p.root = map[string]any{}
p.current = p.root
p.headers = map[string]bool{}
p.frozen = map[string]bool{}
p.dotted = map[string]bool{}
p.arrays = map[string]bool{}
p.currentPath = nil
for {
if err := p.skipBlank(); err != nil {
return nil, err
}
if p.eof() {
break
}
c := p.peek()
switch {
case c == '[':
if err := p.parseTableHeader(); err != nil {
return nil, err
}
default:
if err := p.parseKeyValue(); err != nil {
return nil, err
}
}
if err := p.expectLineEnd(); err != nil {
return nil, err
}
}
return p.root, nil
}
// --- table headers ---------------------------------------------------------
func (p *parser) parseTableHeader() error {
array := false
p.next() // consume '['
if !p.eof() && p.peek() == '[' {
array = true
p.next()
}
key, err := p.parseKeyPath()
if err != nil {
return err
}
p.skipInline()
if p.eof() || p.peek() != ']' {
return p.errf("expected ']' to close table header")
}
p.next()
if array {
if p.eof() || p.peek() != ']' {
return p.errf("expected ']]' to close array-of-tables header")
}
p.next()
}
if array {
tbl, err := p.appendArrayTable(key)
if err != nil {
return err
}
// A new array-of-tables element starts a fresh scope: sub-table headers
// and inline-table freezes from the previous element no longer apply.
p.resetScopeUnder(key)
p.arrays[pathKey(key)] = true
p.current = tbl
p.currentPath = key
return nil
}
pk := pathKey(key)
if p.headers[pk] || p.dotted[pk] || p.arrays[pk] {
return p.errf("table %q is defined more than once", strings.Join(key, "."))
}
p.headers[pk] = true
tbl, err := p.tableAt(key)
if err != nil {
return err
}
p.current = tbl
p.currentPath = key
return nil
}
// tableAt walks (creating intermediate tables) to the table named by key,
// relative to the document root, rejecting any step into a frozen inline table.
func (p *parser) tableAt(key []string) (map[string]any, error) {
cur := p.root
path := make([]string, 0, len(key))
for _, k := range key {
path = append(path, k)
if p.frozen[pathKey(path)] {
return nil, p.errf("cannot extend inline table %q", strings.Join(path, "."))
}
existing, ok := cur[k]
if !ok {
next := map[string]any{}
cur[k] = next
cur = next
continue
}
switch v := existing.(type) {
case map[string]any:
cur = v
case []map[string]any:
if len(v) == 0 {
return nil, p.errf("key %q is an empty array of tables", k)
}
cur = v[len(v)-1]
default:
return nil, p.errf("key %q is not a table", k)
}
}
return cur, nil
}
func (p *parser) appendArrayTable(key []string) (map[string]any, error) {
parent := p.root
for _, k := range key[:len(key)-1] {
existing, ok := parent[k]
if !ok {
next := map[string]any{}
parent[k] = next
parent = next
continue
}
switch v := existing.(type) {
case map[string]any:
parent = v
case []map[string]any:
parent = v[len(v)-1]
default:
return nil, p.errf("key %q is not a table", k)
}
}
leaf := key[len(key)-1]
tbl := map[string]any{}
switch existing := parent[leaf].(type) {
case nil:
parent[leaf] = []map[string]any{tbl}
case []map[string]any:
parent[leaf] = append(existing, tbl)
default:
return nil, p.errf("key %q is not an array of tables", leaf)
}
return tbl, nil
}
// --- key/value -------------------------------------------------------------
func (p *parser) parseKeyValue() error {
key, err := p.parseKeyPath()
if err != nil {
return err
}
p.skipInline()
if p.eof() || p.peek() != '=' {
return p.errf("expected '=' after key")
}
p.next()
p.skipInline()
val, err := p.parseValue()
if err != nil {
return err
}
dest := p.current
abs := append([]string{}, p.currentPath...)
for _, k := range key[:len(key)-1] {
abs = append(abs, k)
if p.frozen[pathKey(abs)] {
return p.errf("cannot extend inline table %q", strings.Join(abs, "."))
}
if p.headers[pathKey(abs)] {
return p.errf("cannot extend table %q with a dotted key", strings.Join(abs, "."))
}
p.dotted[pathKey(abs)] = true
existing, ok := dest[k]
if !ok {
next := map[string]any{}
dest[k] = next
dest = next
continue
}
m, ok := existing.(map[string]any)
if !ok {
return p.errf("key %q is not a table", k)
}
dest = m
}
leaf := key[len(key)-1]
abs = append(abs, leaf)
if _, exists := dest[leaf]; exists {
return p.errf("duplicate key %q", leaf)
}
dest[leaf] = val
p.freezeInline(abs, val)
return nil
}
// freezeInline marks the path of an inline table (and any nested inline tables)
// as immutable, so a later header or dotted key cannot extend it.
func (p *parser) freezeInline(path []string, val any) {
m, ok := val.(map[string]any)
if !ok {
return
}
p.frozen[pathKey(path)] = true
for k, v := range m {
child := append(append([]string{}, path...), k)
p.freezeInline(child, v)
}
}
// resetScopeUnder forgets the header and freeze records nested under key, which
// belong to the previous element of an array of tables.
func (p *parser) resetScopeUnder(key []string) {
prefix := pathKey(key) + "\x00"
for k := range p.headers {
if strings.HasPrefix(k, prefix) {
delete(p.headers, k)
}
}
for k := range p.frozen {
if strings.HasPrefix(k, prefix) {
delete(p.frozen, k)
}
}
}
// parseKeyPath parses a dotted key into its components.
func (p *parser) parseKeyPath() ([]string, error) {
var parts []string
for {
p.skipInline()
part, err := p.parseKeyComponent()
if err != nil {
return nil, err
}
parts = append(parts, part)
p.skipInline()
if !p.eof() && p.peek() == '.' {
p.next()
continue
}
break
}
return parts, nil
}
func (p *parser) parseKeyComponent() (string, error) {
if p.eof() {
return "", p.errf("expected a key")
}
switch c := p.peek(); c {
case '"':
if p.lookahead(`"""`) {
return "", p.errf("multiline strings are not allowed in keys")
}
return p.parseBasicString()
case '\'':
if p.lookahead(`'''`) {
return "", p.errf("multiline strings are not allowed in keys")
}
return p.parseLiteralString()
default:
start := p.pos
for !p.eof() {
c := p.peek()
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-' {
p.next()
continue
}
break
}
if p.pos == start {
return "", p.errf("invalid key character %q", string(p.peek()))
}
return string(p.src[start:p.pos]), nil
}
}
// --- values ----------------------------------------------------------------
func (p *parser) parseValue() (any, error) {
if p.eof() {
return nil, p.errf("expected a value")
}
switch c := p.peek(); {
case c == '"':
return p.parseBasicString()
case c == '\'':
return p.parseLiteralString()
case c == '[':
return p.parseArray()
case c == '{':
return p.parseInlineTable()
case c == 't' || c == 'f':
return p.parseBool()
default:
return p.parseAtom()
}
}
func (p *parser) parseBool() (any, error) {
if p.match("true") {
return true, nil
}
if p.match("false") {
return false, nil
}
return nil, p.errf("invalid value")
}
// parseAtom handles numbers, inf/nan, and date-times.
func (p *parser) parseAtom() (any, error) {
start := p.pos
p.scanBareToken()
tok := string(p.src[start:p.pos])
if tok == "" {
return nil, p.errf("expected a value")
}
// A date may be followed by a space and a time, forming one date-time.
if isDateToken(tok) && !p.eof() && p.peek() == ' ' &&
p.pos+1 < len(p.src) && p.src[p.pos+1] >= '0' && p.src[p.pos+1] <= '9' {
p.next() // consume the separating space
timeStart := p.pos
p.scanBareToken()
tok = tok + " " + string(p.src[timeStart:p.pos])
}
if v, ok := parseDateTime(tok); ok {
return v, nil
}
v, err := decodeNumber(tok)
if err != nil {
return nil, p.errf("%s", err)
}
return v, nil
}
// scanBareToken advances past a bare value token (number, bool, or date-time),
// stopping at whitespace, a separator, or a comment.
func (p *parser) scanBareToken() {
for !p.eof() {
c := p.peek()
if c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
c == ',' || c == ']' || c == '}' || c == '#' {
return
}
p.next()
}
}
// --- strings ---------------------------------------------------------------
func (p *parser) parseBasicString() (string, error) {
if p.lookahead(`"""`) {
return p.parseMultilineString('"', true)
}
p.next() // opening quote
var b strings.Builder
for {
if p.eof() {
return "", p.errf("unterminated string")
}
c := p.next()
switch c {
case '"':
return b.String(), nil
case '\n':
return "", p.errf("unterminated string")
case '\r':
return "", p.errf("bare carriage return is not allowed in a string")
case '\\':
r, err := p.readEscape()
if err != nil {
return "", err
}
b.WriteRune(r)
default:
if isControlRune(c) {
return "", p.errf("control character U+%04X is not allowed in a string", c)
}
b.WriteRune(c)
}
}
}
func (p *parser) parseLiteralString() (string, error) {
if p.lookahead(`'''`) {
return p.parseMultilineString('\'', false)
}
p.next() // opening quote
var b strings.Builder
for {
if p.eof() {
return "", p.errf("unterminated literal string")
}
c := p.next()
if c == '\'' {
return b.String(), nil
}
if c == '\n' {
return "", p.errf("unterminated literal string")
}
if c == '\r' {
return "", p.errf("bare carriage return is not allowed in a string")
}
if isControlRune(c) {
return "", p.errf("control character U+%04X is not allowed in a string", c)
}
b.WriteRune(c)
}
}
func (p *parser) parseMultilineString(quote rune, escapes bool) (string, error) {
p.skipN(3) // opening delimiter
// A newline immediately after the opening delimiter is trimmed.
if !p.eof() && p.peek() == '\r' {
p.next()
}
if !p.eof() && p.peek() == '\n' {
p.line++
p.next()
}
var b strings.Builder
for {
if p.eof() {
return "", p.errf("unterminated multiline string")
}
if p.peek() == quote {
// Count the run of delimiter characters. The last three close the
// string; up to two extra ones belong to the content.
n := 0
for p.pos+n < len(p.src) && p.src[p.pos+n] == quote {
n++
}
if n >= 3 {
if n > 5 {
return "", p.errf("too many '%c' before the closing delimiter", quote)
}
for i := 0; i < n-3; i++ {
b.WriteRune(quote)
}
p.skipN(n)
return b.String(), nil
}
for i := 0; i < n; i++ {
b.WriteRune(quote)
p.next()
}
continue
}
c := p.next()
if c == '\n' {
p.line++
b.WriteRune(c)
continue
}
if c == '\r' {
if !p.eof() && p.peek() == '\n' {
b.WriteRune(c)
continue
}
return "", p.errf("bare carriage return is not allowed in a string")
}
if escapes && c == '\\' {
// Line-ending backslash trims the following whitespace/newlines.
if p.trimLineEndingBackslash() {
continue
}
r, err := p.readEscape()
if err != nil {
return "", err
}
b.WriteRune(r)
continue
}
if isControlRune(c) {
return "", p.errf("control character U+%04X is not allowed in a string", c)
}
b.WriteRune(c)
}
}
// trimLineEndingBackslash consumes whitespace through the next newline (and the
// blank lines that follow) when a backslash is the last token on a line.
// It reports whether it did so.
func (p *parser) trimLineEndingBackslash() bool {
save, saveLine := p.pos, p.line
for !p.eof() {
c := p.peek()
if c == ' ' || c == '\t' || c == '\r' {
p.next()
continue
}
if c == '\n' {
break
}
// Not a line-ending backslash; restore.
p.pos, p.line = save, saveLine
return false
}
if p.eof() {
p.pos, p.line = save, saveLine
return false
}
// Consume the newline and all following whitespace.
for !p.eof() {
c := p.peek()
if c == '\n' {
p.line++
p.next()
continue
}
if c == ' ' || c == '\t' || c == '\r' {
p.next()
continue
}
break
}
return true
}
func (p *parser) readEscape() (rune, error) {
if p.eof() {
return 0, p.errf("unterminated escape sequence")
}
c := p.next()
switch c {
case 'b':
return '\b', nil
case 't':
return '\t', nil
case 'n':
return '\n', nil
case 'f':
return '\f', nil
case 'r':
return '\r', nil
case '"':
return '"', nil
case '\\':
return '\\', nil
case 'u':
return p.readUnicode(4)
case 'U':
return p.readUnicode(8)
default:
return 0, p.errf("invalid escape sequence \\%c", c)
}
}
func (p *parser) readUnicode(n int) (rune, error) {
if p.pos+n > len(p.src) {
return 0, p.errf("invalid unicode escape")
}
hex := string(p.src[p.pos : p.pos+n])
p.pos += n
v, err := strconv.ParseInt(hex, 16, 64)
if err != nil {
return 0, p.errf("invalid unicode escape \\%s", hex)
}
if v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF) {
return 0, p.errf("escape \\%s is not a valid Unicode scalar value", hex)
}
return rune(v), nil
}
// --- arrays and inline tables ---------------------------------------------
func (p *parser) parseArray() (any, error) {
p.next() // '['
arr := []any{}
for {
if err := p.skipArraySpace(); err != nil {
return nil, err
}
if p.eof() {
return nil, p.errf("unterminated array")
}
if p.peek() == ']' {
p.next()
return arr, nil
}
v, err := p.parseValue()
if err != nil {
return nil, err
}
arr = append(arr, v)
if err := p.skipArraySpace(); err != nil {
return nil, err
}
if p.eof() {
return nil, p.errf("unterminated array")
}
switch p.peek() {
case ',':
p.next()
case ']':
p.next()
return arr, nil
default:
return nil, p.errf("expected ',' or ']' in array")
}
}
}
func (p *parser) parseInlineTable() (any, error) {
p.next() // '{'
tbl := map[string]any{}
assigned := map[string]bool{}
p.skipInline()
if !p.eof() && p.peek() == '}' {
p.next()
return tbl, nil
}
for {
p.skipInline()
key, err := p.parseKeyPath()
if err != nil {
return nil, err
}
p.skipInline()
if p.eof() || p.peek() != '=' {
return nil, p.errf("expected '=' in inline table")
}
p.next()
p.skipInline()
val, err := p.parseValue()
if err != nil {
return nil, err
}
dest := tbl
path := make([]string, 0, len(key))
for _, k := range key[:len(key)-1] {
path = append(path, k)
if assigned[pathKey(path)] {
return nil, p.errf("key %q is already defined", strings.Join(path, "."))
}
existing, ok := dest[k]
if !ok {
m := map[string]any{}
dest[k] = m
dest = m
continue
}
m, isMap := existing.(map[string]any)
if !isMap {
return nil, p.errf("key %q is already defined", k)
}
dest = m
}
leaf := key[len(key)-1]
path = append(path, leaf)
if _, exists := dest[leaf]; exists {
return nil, p.errf("duplicate key %q in inline table", leaf)
}
dest[leaf] = val
assigned[pathKey(path)] = true
p.skipInline()
if p.eof() {
return nil, p.errf("unterminated inline table")
}
switch p.peek() {
case ',':
p.next()
case '}':
p.next()
return tbl, nil
default:
return nil, p.errf("expected ',' or '}' in inline table")
}
}
}
// --- scanning helpers ------------------------------------------------------
func (p *parser) eof() bool { return p.pos >= len(p.src) }
func (p *parser) peek() rune { return p.src[p.pos] }
func (p *parser) next() rune {
c := p.src[p.pos]
p.pos++
return c
}
func (p *parser) skipN(n int) {
for i := 0; i < n && !p.eof(); i++ {
p.next()
}
}
func (p *parser) match(word string) bool {
if p.lookahead(word) {
p.skipN(len([]rune(word)))
return true
}
return false
}
func (p *parser) lookahead(s string) bool {
r := []rune(s)
if p.pos+len(r) > len(p.src) {
return false
}
for i, c := range r {
if p.src[p.pos+i] != c {
return false
}
}
return true
}
// skipInline consumes spaces and tabs only.
func (p *parser) skipInline() {
for !p.eof() {
if c := p.peek(); c == ' ' || c == '\t' {
p.next()
continue
}
break
}
}
// skipArraySpace consumes whitespace, newlines, and comments inside arrays.
func (p *parser) skipArraySpace() error {
for !p.eof() {
switch p.peek() {
case ' ', '\t':
p.next()
case '\r':
if err := p.expectCRLF(); err != nil {
return err
}
case '\n':
p.line++
p.next()
case '#':
if err := p.skipComment(); err != nil {
return err
}
default:
return nil
}
}
return nil
}
// skipBlank consumes whitespace, blank lines, and comments between statements.
func (p *parser) skipBlank() error {
for !p.eof() {
switch p.peek() {
case ' ', '\t':
p.next()
case '\r':
if err := p.expectCRLF(); err != nil {
return err
}
case '\n':
p.line++
p.next()
case '#':
if err := p.skipComment(); err != nil {
return err
}
default:
return nil
}
}
return nil
}
func (p *parser) skipComment() error {
p.next() // consume '#'
for !p.eof() {
c := p.peek()
switch {
case c == '\n':
return nil
case c == '\r':
if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' {
return nil
}
return p.errf("bare carriage return is not allowed")
case c == '\t':
p.next()
case c < 0x20 || c == 0x7f:
return p.errf("control character U+%04X is not allowed in a comment", c)
default:
p.next()
}
}
return nil
}
// expectCRLF consumes a carriage return that must be immediately followed by a
// line feed; a bare CR is invalid.
func (p *parser) expectCRLF() error {
if p.pos+1 < len(p.src) && p.src[p.pos+1] == '\n' {
p.next() // consume CR; the LF is handled by the caller
return nil
}
return p.errf("bare carriage return is not allowed")
}
// expectLineEnd consumes trailing inline whitespace and an optional comment,
// then requires a newline or end of input.
func (p *parser) expectLineEnd() error {
p.skipInline()
if p.eof() {
return nil
}
if p.peek() == '#' {
if err := p.skipComment(); err != nil {
return err
}
}
if p.eof() {
return nil
}
if p.peek() == '\r' {
if err := p.expectCRLF(); err != nil {
return err
}
}
if p.eof() {
return nil
}
if p.peek() == '\n' {
p.line++
p.next()
return nil
}
return p.errf("unexpected %q after value", string(p.peek()))
}
func (p *parser) errf(format string, args ...any) error {
return &SyntaxError{Line: p.line, Msg: fmt.Sprintf(format, args...)}
}
// pathKey joins key components with a NUL separator so a dotted path can be
// used as a map key for tracking defined tables.
func pathKey(parts []string) string {
return strings.Join(parts, "\x00")
}
// isControlRune reports whether r is a control character disallowed in a string
// literal. Tab, line feed, and carriage return are permitted (handled
// elsewhere); everything else below U+0020, plus U+007F, is rejected.
func isControlRune(r rune) bool {
if r == '\t' || r == '\n' || r == '\r' {
return false
}
return r < 0x20 || r == 0x7f
}