aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--config.ebnf28
-rw-r--r--doc.go2
-rw-r--r--example/file.conf33
-rw-r--r--example/main.go16
-rw-r--r--gen.go3
-rw-r--r--go.mod7
-rw-r--r--go.sum4
-rw-r--r--internal/ast/processor.go121
-rw-r--r--internal/ast/tree.go43
-rw-r--r--internal/parser/parser.go992
-rw-r--r--loader.go36
-rw-r--r--model/doc.go18
12 files changed, 1303 insertions, 0 deletions
diff --git a/config.ebnf b/config.ebnf
new file mode 100644
index 0000000..965c5c7
--- /dev/null
+++ b/config.ebnf
@@ -0,0 +1,28 @@
+Config = Doc .
+Doc = Stmt { Stmt } .
+Stmt = Word (Assignment | Command) .
+
+# Statements
+Assignment = "=" Values br .
+Command = [Values] ( Body | br ) .
+
+# Value types
+Values = Value {Value} .
+Value = Word | String | Number | Boolean .
+Body = lbrace [ Doc ] rbrace .
+
+# Atoms
+Word = word .
+Number = number .
+Boolean = boolean .
+String = str .
+
+# Primitives
+word = `[a-zA-Z_][a-zA-Z0-9_]*` .
+number = `-?[0-9]+(\.[0-9]+)?` .
+boolean = `true|false` .
+str = `"[^"]*"` | `'[^']*'` | '`' { `[^\x60]` } '`' .
+lbrace = "{" .
+rbrace = "}" .
+br = ";" .
+white_space = ` |\t|\r|\n|#.*` .
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..51419d4
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,2 @@
+// Package conf is a library to read conf format files.
+package conf
diff --git a/example/file.conf b/example/file.conf
new file mode 100644
index 0000000..d23c7d7
--- /dev/null
+++ b/example/file.conf
@@ -0,0 +1,33 @@
+# one line comment
+
+simpe_key = "тест_кириллицы";
+simple_key_without_spaces = value;
+
+string_key =
+ "value"
+ 'string';
+
+multiline_string = `
+ multiline
+ string
+ 123
+`;
+
+int_key = -123.456;
+bool_key = true;
+
+expression1 argument1 "argument2" 123;
+
+# comment can be everywhere
+group_directive_without_arguments {
+ expression1 argument2 "string" 123 true;
+ expression2 argument3 "string111" 123321 false;
+ children_group "some argument" {
+ # child group. Can be empty. This is equivalent to directive `children_group "some argument"`
+ }
+}
+
+group_directive_with_argument "some other argument" 'second argument' {
+ child_val = "children value";
+}
+
diff --git a/example/main.go b/example/main.go
new file mode 100644
index 0000000..6e0c8b8
--- /dev/null
+++ b/example/main.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+ "fmt"
+
+ "go.neonxp.ru/conf"
+)
+
+func main() {
+ out, err := conf.LoadFile("./file.conf")
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(out)
+}
diff --git a/gen.go b/gen.go
new file mode 100644
index 0000000..04c9e93
--- /dev/null
+++ b/gen.go
@@ -0,0 +1,3 @@
+package conf
+
+//go:generate egg -o ./internal/parser/parser.go -package parser -start Config config.ebnf
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c1e462d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,7 @@
+module go.neonxp.ru/conf
+
+go 1.25.0
+
+require modernc.org/scanner v1.3.0
+
+require modernc.org/token v1.1.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..08246e5
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+modernc.org/scanner v1.3.0 h1:930qF5E11ZENhAV27sw601w5eVaKZIlC1JJdPCS1pS8=
+modernc.org/scanner v1.3.0/go.mod h1:dLuOdK1HO0HLmTSj29XOm62Oq3Y7Gbg92YmA16ZkKKQ=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/internal/ast/processor.go b/internal/ast/processor.go
new file mode 100644
index 0000000..89ffbff
--- /dev/null
+++ b/internal/ast/processor.go
@@ -0,0 +1,121 @@
+package ast
+
+import (
+ "fmt"
+ "strconv"
+
+ "go.neonxp.ru/conf/internal/parser"
+ "go.neonxp.ru/conf/model"
+)
+
+func ToDoc(config *Node) (model.Doc, error) {
+ if len(config.Children) < 1 {
+ return nil, fmt.Errorf("invalid ast tree")
+ }
+
+ doc := config.Children[0]
+
+ return processDoc(doc), nil
+}
+
+func processDoc(docNode *Node) model.Doc {
+ doc := make(model.Doc, len(docNode.Children))
+ for i, stmt := range docNode.Children {
+ doc[i] = processStmt(stmt)
+ }
+ return doc
+}
+
+func processStmt(stmt *Node) any {
+ ident := extractIdent(stmt.Children[0])
+ nodeBody := stmt.Children[1]
+ switch nodeBody.Symbol {
+ case parser.Command:
+ return processCommand(ident, nodeBody)
+ case parser.Assignment:
+ return processAssignment(ident, nodeBody)
+ default:
+ return nil
+ }
+}
+
+func processCommand(ident string, command *Node) *model.Command {
+ result := &model.Command{
+ Name: ident,
+ }
+
+ for _, child := range command.Children {
+ // Can be arguments OR body OR both
+ switch child.Symbol {
+ case parser.Values:
+ result.Arguments = extractValues(child)
+ case parser.Body:
+ // Children[0] = '{', Children[1] = Body, Children[2] = '}'
+ result.Body = processDoc(child.Children[1])
+ }
+ }
+
+ return result
+}
+
+func processAssignment(ident string, assignment *Node) *model.Assignment {
+ result := &model.Assignment{
+ Key: ident,
+ Value: extractValues(assignment.Children[1]), // Children[0] = '=', Children[1] = Values, Children[2] = ';'
+ }
+
+ return result
+}
+
+func extractIdent(word *Node) string {
+ return word.Children[0].Source
+}
+
+func extractValues(args *Node) []model.Value {
+ result := make([]model.Value, len(args.Children))
+ for i, child := range args.Children {
+ v, err := extractValue(child)
+ if err != nil {
+ result[i] = err
+ continue
+ }
+ result[i] = v
+ }
+
+ return result
+}
+
+func extractValue(value *Node) (any, error) {
+ v := value.Children[0]
+ s := v.Children[0].Source
+ switch v.Symbol {
+ case parser.Word:
+ return model.Word(s), nil
+ case parser.String:
+ return unquote(s), nil
+ case parser.Number:
+ d, err := strconv.Atoi(s)
+ if err == nil {
+ return d, nil
+ }
+ fl, err := strconv.ParseFloat(s, 32)
+ if err == nil {
+ return fl, nil
+ }
+ return nil, fmt.Errorf("invalid number: %s (%s)", v.Source, s)
+ case parser.Boolean:
+ return s == "true", nil
+ default:
+ return nil, fmt.Errorf("unknown value type: %s (%s)", v.Symbol, s)
+ }
+}
+
+func unquote(str string) string {
+ if len(str) == 0 {
+ return ""
+ }
+ if str[0:1] == `"` || str[0:1] == "'" || str[0:1] == "`" {
+ return str[1 : len(str)-1]
+ }
+ return str
+}
diff --git a/internal/ast/tree.go b/internal/ast/tree.go
new file mode 100644
index 0000000..72b7c78
--- /dev/null
+++ b/internal/ast/tree.go
@@ -0,0 +1,43 @@
+package ast
+
+import (
+ "go.neonxp.ru/conf/internal/parser"
+ "modernc.org/scanner"
+)
+
+func Parse(p *parser.Parser, data []int32) []*Node {
+ nodes := make([]*Node, 0, 2)
+ for len(data) != 0 {
+ next := int32(1)
+ var node *Node
+ switch n := data[0]; {
+ case n < 0:
+ next = 2 + data[1]
+ node = &Node{
+ Symbol: parser.Symbol(-data[0]),
+ Children: Parse(p, data[2:next]),
+ }
+ default:
+ tok := p.Token(n)
+ node = &Node{
+ Token: tok,
+ Symbol: parser.Symbol(tok.Ch),
+ Source: tok.Src(),
+ Col: tok.Position().Column,
+ Line: tok.Position().Line,
+ }
+ }
+ nodes = append(nodes, node)
+ data = data[next:]
+ }
+ return nodes
+}
+
+type Node struct {
+ Symbol parser.Symbol
+ Token scanner.Token
+ Children []*Node
+ Source string
+ Col int
+ Line int
+}
diff --git a/internal/parser/parser.go b/internal/parser/parser.go
new file mode 100644
index 0000000..c1785f6
--- /dev/null
+++ b/internal/parser/parser.go
@@ -0,0 +1,992 @@
+// Code generated by 'egg -o ./internal/parser/parser.go -package parser -start Config config.ebnf', DO NOT EDIT.
+
+// Parser generated from config.ebnf.
+
+package parser
+
+import (
+ "errors"
+ "fmt"
+ "go/token"
+ "strconv"
+ "unicode"
+ "unicode/utf8"
+
+ "modernc.org/scanner"
+)
+
+var _ = unicode.MaxRune
+
+
+
+
+
+// Symbols
+const (
+ TOK_EOF = Symbol(0) // EOF
+ TOK_003d = Symbol(1) // '='
+ boolean = Symbol(2) // boolean
+ br = Symbol(3) // br
+ lbrace = Symbol(4) // lbrace
+ number = Symbol(5) // number
+ rbrace = Symbol(6) // rbrace
+ str = Symbol(7) // str
+ white_space = Symbol(8) // white_space
+ word = Symbol(9) // word
+ Config = Symbol(10) // Config
+ Doc = Symbol(11) // Doc
+ Stmt = Symbol(12) // Stmt
+ Assignment = Symbol(13) // Assignment
+ Command = Symbol(14) // Command
+ Values = Symbol(15) // Values
+ Value = Symbol(16) // Value
+ Body = Symbol(17) // Body
+ Word = Symbol(18) // Word
+ Number = Symbol(19) // Number
+ Boolean = Symbol(20) // Boolean
+ String = Symbol(21) // String
+)
+
+const SymbolNames = "EOF'='booleanbrlbracenumberrbracestrwhite_spacewordConfigDocStmtAssignmentCommandValuesValueBodyWordNumberBooleanString"
+
+var SymbolIndex = [...]uint8{0, 3, 6, 13, 15, 21, 27, 33, 36, 47, 51, 57, 60, 64, 74, 81, 87, 92, 96, 100, 106, 113, 119, }
+
+
+func (s Symbol) String() string {
+ idx := int(s) - 0
+ if s < 0 || idx >= len(SymbolIndex)-1 {
+ return "Symbol(" + strconv.FormatInt(int64(s), 10) + ")"
+ }
+ return SymbolNames[SymbolIndex[idx]:SymbolIndex[idx+1]]
+}
+
+var errorSets = [...][]Symbol{
+{String, Boolean, Number, Word, Body, Value, Values, word, str, number, lbrace, br, boolean, TOK_003d},
+{TOK_003d},
+{String, Boolean, Number, Word, Value, word, str, number, lbrace, br, boolean},
+{String, Boolean, Number, Word, Value, word, str, number, boolean},
+{String, Boolean, Number, Word, word, str, number, boolean},
+{word, str, number, boolean},
+{boolean},
+{lbrace, br},
+{br},
+{lbrace},
+{number},
+{Word, Stmt, word, rbrace},
+{rbrace},
+{str},
+{Word, Stmt, word},
+{Word, word},
+{word},
+}
+
+
+
+type Parser struct{
+ cache [][]int32
+ eof bool
+ errBudget int
+ id rune // from scanSep, valid if .n != 0
+ n int // from scanSep, valid if != 0
+ off int
+ sc *scanner.RecScanner
+ src []byte
+ tok scanner.Token // current lookahead
+ tokIndex int32 // For scanner.Token(tokIndex)
+}
+
+type Symbol int32
+
+
+// scan recognizes longest UTF-8 lexemes. Lower IDs take precedence on same length.
+//
+// id 0: $
+// id 1: =
+// id 2: (true|false)
+// id 3: (;)
+// id 4: (\{)
+// id 5: (-?[0-9]+(\.[0-9]+)?)
+// id 6: (\})
+// id 7: (("[^"]*")|('[^']*')|(`)(([^\x60]))*(`))
+// id 8: ( |\t|\r|\n|#.*)
+// id 9: ([a-zA-Z_][a-zA-Z0-9_]*)
+//
+// ID == -1 is returned when no lexeme was recognized.
+func (*Parser) scan(s []byte) (id, length int) {
+ const endOfText = 0x110000
+ var pos, pos0, width, width1 int
+ id = -1
+ var r, r1 rune
+ _ = pos0
+ _ = r
+ _ = r1
+ _ = width1
+ step := func(pos int) (r rune, n int) { if pos < len(s) { c := s[pos]; if c < utf8.RuneSelf { return rune(c), 1 }; return utf8.DecodeRune(s[pos:]) }; return endOfText, 0 }
+ move := func() { pos += width; if r, width = r1, width1; r != endOfText { r1, width1 = step(pos+width); }; }
+ accept := func(x rune) bool { if r == x { move(); return true }; return false }
+_ = accept
+ accept2 := func(x rune) bool { if r <= x { move(); return true }; return false }
+_ = accept2
+ r, r1 = endOfText, endOfText
+ width, width1 = 0, 0
+ r, width = step(pos); if r != endOfText {
+ r1, width1 = step(pos+width); }
+ if accept('\t') { goto l37 }
+ if accept('\n') { goto l39 }
+ if accept('\r') { goto l41 }
+ if accept(' ') { goto l43 }
+ if accept('"') { goto l45 }
+ if accept('#') { goto l57 }
+ if accept('\'') { goto l65 }
+ if accept('-') { goto l77 }
+ if accept(';') { goto l93 }
+ if accept('=') { goto l95 }
+ if accept('`') { goto l105 }
+ if accept('f') { goto l117 }
+ if accept('t') { goto l145 }
+ if accept('{') { goto l167 }
+ if accept('}') { goto l169 }
+ if r < '0' { goto l30out }
+ if accept2('9') { goto l80 }
+l30out:
+ if r < 'A' { goto l32out }
+ if accept2('Z') { goto l97 }
+ if accept('_') { goto l97 }
+ if r < 'a' { goto l32out }
+ if accept2('e') { goto l97 }
+ if r < 'g' { goto l32out }
+ if accept2('s') { goto l97 }
+ if r < 'u' { goto l32out }
+ if accept2('z') { goto l97 }
+l32out:
+ if r == endOfText { goto l171 }
+ return id, length
+l37:
+ id, length = 8, pos
+ return id, length
+l39:
+ id, length = 8, pos
+ return id, length
+l41:
+ id, length = 8, pos
+ return id, length
+l43:
+ id, length = 8, pos
+ return id, length
+l45:
+ if accept('"') { goto l50 }
+ if accept2('!') { goto l52 }
+ if r < '#' { goto l47out }
+ if accept2('\U0010ffff') { goto l52 }
+l47out:
+ return id, length
+l50:
+ id, length = 7, pos
+ return id, length
+l52:
+ if accept('"') { goto l50 }
+ if accept2('!') { goto l52 }
+ if r < '#' { goto l54out }
+ if accept2('\U0010ffff') { goto l52 }
+l54out:
+ return id, length
+l57:
+ id, length = 8, pos
+ if accept2('\t') { goto l61 }
+ if r < '\v' { goto l58out }
+ if accept2('\U0010ffff') { goto l61 }
+l58out:
+ return id, length
+l61:
+ id, length = 8, pos
+ if accept2('\t') { goto l61 }
+ if r < '\v' { goto l62out }
+ if accept2('\U0010ffff') { goto l61 }
+l62out:
+ return id, length
+l65:
+ if accept('\'') { goto l70 }
+ if accept2('&') { goto l72 }
+ if r < '(' { goto l67out }
+ if accept2('\U0010ffff') { goto l72 }
+l67out:
+ return id, length
+l70:
+ id, length = 7, pos
+ return id, length
+l72:
+ if accept('\'') { goto l70 }
+ if accept2('&') { goto l72 }
+ if r < '(' { goto l74out }
+ if accept2('\U0010ffff') { goto l72 }
+l74out:
+ return id, length
+l77:
+ if r < '0' { goto l77out }
+ if accept2('9') { goto l80 }
+l77out:
+ return id, length
+l80:
+ id, length = 5, pos
+ if accept('.') { goto l86 }
+ if r < '0' { goto l83out }
+ if accept2('9') { goto l80 }
+l83out:
+ return id, length
+l86:
+ if r < '0' { goto l86out }
+ if accept2('9') { goto l89 }
+l86out:
+ return id, length
+l89:
+ id, length = 5, pos
+ if r < '0' { goto l90out }
+ if accept2('9') { goto l89 }
+l90out:
+ return id, length
+l93:
+ id, length = 3, pos
+ return id, length
+l95:
+ id, length = 1, pos
+ return id, length
+l97:
+ id, length = 9, pos
+ if r < '0' { goto l98out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l98out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l98out }
+ if accept2('z') { goto l101 }
+l98out:
+ return id, length
+l101:
+ id, length = 9, pos
+ if r < '0' { goto l102out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l102out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l102out }
+ if accept2('z') { goto l101 }
+l102out:
+ return id, length
+l105:
+ if accept('`') { goto l110 }
+ if accept2('_') { goto l112 }
+ if r < 'a' { goto l107out }
+ if accept2('\U0010ffff') { goto l112 }
+l107out:
+ return id, length
+l110:
+ id, length = 7, pos
+ return id, length
+l112:
+ if accept('`') { goto l110 }
+ if accept2('_') { goto l112 }
+ if r < 'a' { goto l114out }
+ if accept2('\U0010ffff') { goto l112 }
+l114out:
+ return id, length
+l117:
+ id, length = 9, pos
+ if accept('a') { goto l123 }
+ if r < '0' { goto l120out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l120out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'b' { goto l120out }
+ if accept2('z') { goto l101 }
+l120out:
+ return id, length
+l123:
+ id, length = 9, pos
+ if accept('l') { goto l129 }
+ if r < '0' { goto l126out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l126out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l126out }
+ if accept2('k') { goto l101 }
+ if r < 'm' { goto l126out }
+ if accept2('z') { goto l101 }
+l126out:
+ return id, length
+l129:
+ id, length = 9, pos
+ if accept('s') { goto l135 }
+ if r < '0' { goto l132out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l132out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l132out }
+ if accept2('r') { goto l101 }
+ if r < 't' { goto l132out }
+ if accept2('z') { goto l101 }
+l132out:
+ return id, length
+l135:
+ id, length = 9, pos
+ if accept('e') { goto l141 }
+ if r < '0' { goto l138out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l138out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l138out }
+ if accept2('d') { goto l101 }
+ if r < 'f' { goto l138out }
+ if accept2('z') { goto l101 }
+l138out:
+ return id, length
+l141:
+ id, length = 2, pos
+ if r < '0' { goto l142out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l142out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l142out }
+ if accept2('z') { goto l101 }
+l142out:
+ return id, length
+l145:
+ id, length = 9, pos
+ if accept('r') { goto l151 }
+ if r < '0' { goto l148out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l148out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l148out }
+ if accept2('q') { goto l101 }
+ if r < 's' { goto l148out }
+ if accept2('z') { goto l101 }
+l148out:
+ return id, length
+l151:
+ id, length = 9, pos
+ if accept('u') { goto l157 }
+ if r < '0' { goto l154out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l154out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l154out }
+ if accept2('t') { goto l101 }
+ if r < 'v' { goto l154out }
+ if accept2('z') { goto l101 }
+l154out:
+ return id, length
+l157:
+ id, length = 9, pos
+ if accept('e') { goto l163 }
+ if r < '0' { goto l160out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l160out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l160out }
+ if accept2('d') { goto l101 }
+ if r < 'f' { goto l160out }
+ if accept2('z') { goto l101 }
+l160out:
+ return id, length
+l163:
+ id, length = 2, pos
+ if r < '0' { goto l164out }
+ if accept2('9') { goto l101 }
+ if r < 'A' { goto l164out }
+ if accept2('Z') { goto l101 }
+ if accept('_') { goto l101 }
+ if r < 'a' { goto l164out }
+ if accept2('z') { goto l101 }
+l164out:
+ return id, length
+l167:
+ id, length = 4, pos
+ return id, length
+l169:
+ id, length = 6, pos
+ return id, length
+l171:
+ id, length = 0, pos
+ return id, length
+}
+
+
+// Scan is used internally from Parse.
+func(p *Parser) Scan() (r scanner.Token) {
+ return p.sc.Scan()
+}
+
+// init initalizes 'p' with content in 'src', assuming it comes from 'name'.
+// 'src' becomes "owned" by 'p'. init invalidates any pre-existing ASTs produced by 'p'. Mutating
+// 'src' invalidates the current AST returned from any parsing function called after init.
+func (p *Parser) init(name string, src []byte) (err error) {
+ p.eof = false
+ p.errBudget = 10
+ p.n = 0
+ p.off = 0
+ p.src = src
+ p.tok = scanner.Token{}
+ p.tokIndex = 0
+ p.sc = scanner.NewRecScanner(name, p.src, p.scan, int(white_space))
+ return nil
+}
+
+
+
+// Assignment grammar:
+//
+//
+//
+// # Statements
+// Assignment = "=" Values br .
+//
+// State 0
+// on '='
+// shift and goto state 1
+// State 1
+// on boolean, number, str, word, Value, Word, Number, Boolean, String
+// call Values and goto state 2
+// State 2
+// on br
+// shift and goto state 3
+// State 3
+// Accept
+// Assignment is used internally from Parse.
+func (p *Parser) Assignment() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Assignment), 0)
+// state0:
+accept, errorSet = false, 1
+switch Symbol(p.tok.Ch) {
+ case TOK_003d:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = false, 3
+switch Symbol(p.tok.Ch) {
+ case boolean, number, str, word, Value, Word, Number, Boolean, String:
+r = p.add(r, p.Values())
+goto state2
+}
+return p.stop(r, accept, errorSet)
+state2:
+accept, errorSet = false, 8
+switch Symbol(p.tok.Ch) {
+ case br:
+r = append(r, p.shift())
+goto state3
+}
+return p.stop(r, accept, errorSet)
+state3:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// Body grammar:
+//
+//
+// Body = lbrace [ Doc ] rbrace .
+//
+// State 0
+// on lbrace
+// shift and goto state 1
+// State 1
+// on rbrace
+// shift and goto state 2
+// on word, Stmt, Word
+// call Doc and goto state 3
+// State 2
+// Accept
+// State 3
+// on rbrace
+// shift and goto state 2
+// Body is used internally from Parse.
+func (p *Parser) Body() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Body), 0)
+// state0:
+accept, errorSet = false, 9
+switch Symbol(p.tok.Ch) {
+ case lbrace:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = false, 11
+switch Symbol(p.tok.Ch) {
+ case rbrace:
+r = append(r, p.shift())
+goto state2
+ case word, Stmt, Word:
+r = p.add(r, p.Doc())
+goto state3
+}
+return p.stop(r, accept, errorSet)
+state2:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+state3:
+accept, errorSet = false, 12
+switch Symbol(p.tok.Ch) {
+ case rbrace:
+r = append(r, p.shift())
+goto state2
+}
+return p.stop(r, accept, errorSet)
+}
+
+
+// Boolean grammar:
+//
+//
+// Boolean = boolean .
+//
+// State 0
+// on boolean
+// shift and goto state 1
+// State 1
+// Accept
+// Boolean is used internally from Parse.
+func (p *Parser) Boolean() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Boolean), 0)
+// state0:
+accept, errorSet = false, 6
+switch Symbol(p.tok.Ch) {
+ case boolean:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// Command grammar:
+//
+//
+// Command = [Values] ( Body | br ) .
+//
+// State 0
+// on br
+// shift and goto state 1
+// on boolean, number, str, word, Value, Word, Number, Boolean, String
+// call Values and goto state 2
+// on lbrace
+// call Body and goto state 1
+// State 1
+// Accept
+// State 2
+// on br
+// shift and goto state 1
+// on lbrace
+// call Body and goto state 1
+// Command is used internally from Parse.
+func (p *Parser) Command() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Command), 0)
+// state0:
+accept, errorSet = false, 2
+switch Symbol(p.tok.Ch) {
+ case br:
+r = append(r, p.shift())
+goto state1
+ case boolean, number, str, word, Value, Word, Number, Boolean, String:
+r = p.add(r, p.Values())
+goto state2
+ case lbrace:
+r = p.add(r, p.Body())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+state2:
+accept, errorSet = false, 7
+switch Symbol(p.tok.Ch) {
+ case br:
+r = append(r, p.shift())
+goto state1
+ case lbrace:
+r = p.add(r, p.Body())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+}
+
+
+// Config grammar:
+//
+// Config = Doc .
+//
+// State 0
+// on word, Stmt, Word
+// call Doc and goto state 1
+// State 1
+// Accept
+// Config is used internally from Parse.
+func (p *Parser) Config() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Config), 0)
+// state0:
+accept, errorSet = false, 14
+switch Symbol(p.tok.Ch) {
+ case word, Stmt, Word:
+r = p.add(r, p.Doc())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+ if accept = accept && p.eof; accept {
+ r = append(r, p.shift())
+ }
+ r[1] = int32(len(r)-2)
+ if !accept {
+ p.err(p.tok.Position(), "%q [%s]: expected %v", p.tok.Src(), Symbol(p.tok.Ch), errorSets[errorSet])
+ }
+return r
+}
+
+
+// Doc grammar:
+//
+//
+// Doc = Stmt { Stmt } .
+//
+// State 0
+// on word, Word
+// call Stmt and goto state 1
+// State 1
+// Accept
+// on word, Word
+// call Stmt and goto state 1
+// Doc is used internally from Parse.
+func (p *Parser) Doc() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Doc), 0)
+// state0:
+accept, errorSet = false, 15
+switch Symbol(p.tok.Ch) {
+ case word, Word:
+r = p.add(r, p.Stmt())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 15
+switch Symbol(p.tok.Ch) {
+ case word, Word:
+r = p.add(r, p.Stmt())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+}
+
+
+// Number grammar:
+//
+//
+// Number = number .
+//
+// State 0
+// on number
+// shift and goto state 1
+// State 1
+// Accept
+// Number is used internally from Parse.
+func (p *Parser) Number() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Number), 0)
+// state0:
+accept, errorSet = false, 10
+switch Symbol(p.tok.Ch) {
+ case number:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// Stmt grammar:
+//
+//
+// Stmt = Word (Assignment | Command) .
+//
+// State 0
+// on word
+// call Word and goto state 1
+// State 1
+// on '='
+// call Assignment and goto state 2
+// on boolean, br, lbrace, number, str, word, Values, Value, Body, Word, Number, Boolean, String
+// call Command and goto state 2
+// State 2
+// Accept
+// Stmt is used internally from Parse.
+func (p *Parser) Stmt() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Stmt), 0)
+// state0:
+accept, errorSet = false, 16
+switch Symbol(p.tok.Ch) {
+ case word:
+r = p.add(r, p.Word())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = false, 0
+switch Symbol(p.tok.Ch) {
+ case TOK_003d:
+r = p.add(r, p.Assignment())
+goto state2
+ case boolean, br, lbrace, number, str, word, Values, Value, Body, Word, Number, Boolean, String:
+r = p.add(r, p.Command())
+goto state2
+}
+return p.stop(r, accept, errorSet)
+state2:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// String grammar:
+//
+//
+// String = str .
+//
+// State 0
+// on str
+// shift and goto state 1
+// State 1
+// Accept
+// String is used internally from Parse.
+func (p *Parser) String() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(String), 0)
+// state0:
+accept, errorSet = false, 13
+switch Symbol(p.tok.Ch) {
+ case str:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// Value grammar:
+//
+//
+// Value = Word | String | Number | Boolean .
+//
+// State 0
+// on word
+// call Word and goto state 1
+// on number
+// call Number and goto state 1
+// on boolean
+// call Boolean and goto state 1
+// on str
+// call String and goto state 1
+// State 1
+// Accept
+// Value is used internally from Parse.
+func (p *Parser) Value() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Value), 0)
+// state0:
+accept, errorSet = false, 5
+switch Symbol(p.tok.Ch) {
+ case word:
+r = p.add(r, p.Word())
+goto state1
+ case number:
+r = p.add(r, p.Number())
+goto state1
+ case boolean:
+r = p.add(r, p.Boolean())
+goto state1
+ case str:
+r = p.add(r, p.String())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+// Values grammar:
+//
+//
+//
+// # Value types
+// Values = Value {Value} .
+//
+// State 0
+// on boolean, number, str, word, Word, Number, Boolean, String
+// call Value and goto state 1
+// State 1
+// Accept
+// on boolean, number, str, word, Word, Number, Boolean, String
+// call Value and goto state 1
+// Values is used internally from Parse.
+func (p *Parser) Values() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Values), 0)
+// state0:
+accept, errorSet = false, 4
+switch Symbol(p.tok.Ch) {
+ case boolean, number, str, word, Word, Number, Boolean, String:
+r = p.add(r, p.Value())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 4
+switch Symbol(p.tok.Ch) {
+ case boolean, number, str, word, Word, Number, Boolean, String:
+r = p.add(r, p.Value())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+}
+
+
+// Word grammar:
+//
+//
+//
+// # Atoms
+// Word = word .
+//
+// State 0
+// on word
+// shift and goto state 1
+// State 1
+// Accept
+// Word is used internally from Parse.
+func (p *Parser) Word() (r []int32) {
+ accept, errorSet := false, 0
+ r = append(p.get(), -int32(Word), 0)
+// state0:
+accept, errorSet = false, 16
+switch Symbol(p.tok.Ch) {
+ case word:
+r = append(r, p.shift())
+goto state1
+}
+return p.stop(r, accept, errorSet)
+state1:
+accept, errorSet = true, 0
+return p.stop(r, accept, errorSet)
+}
+
+
+func (p *Parser) shift() (r int32) {
+ r = p.tokIndex
+ if !p.eof {
+ p.tok = p.Scan()
+ p.tokIndex++
+ p.eof = p.tok.Ch == rune(TOK_EOF)
+ }
+ return r
+}
+
+func (p *Parser) get() (r []int32) {
+ if n := len(p.cache); n != 0 {
+ r = p.cache[n-1][:0]
+ p.cache = p.cache[:n-1]
+ }
+ return r
+}
+
+func (p *Parser) add(r, s []int32) (t []int32) {
+ p.cache = append(p.cache, s)
+ return append(r, s...)
+}
+
+func (p *Parser) stop(r []int32, accept bool, errorSet int) []int32 {
+ r[1] = int32(len(r) - 2)
+ if !accept {
+ p.err(p.tok.Position(), "%q [%s]: expected %v", p.tok.Src(), Symbol(p.tok.Ch), errorSets[errorSet])
+ }
+ return r
+}
+
+// EOF reports whether the parser lookahead token is EOF.
+func (p *Parser) EOF() bool {
+ return p.eof
+}
+
+// Token returns the n-th token in 'p'. Token panics if 'n' is out of range.
+func (p *Parser) Token(n int32) (r scanner.Token) {
+ return p.sc.Token(int(n))
+}
+
+// Parse parses 'src'. 'src' becomes "owned" by the parser and must not be
+// mutated afterwards.
+func (p *Parser) Parse(name string, src []byte) (ast []int32, err error) {
+ if err = p.init(name, src); err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ switch e := recover(); x := e.(type) {
+ case nil:
+ // ok
+ case scanner.ErrList:
+ err = x
+ case error:
+ err = errors.Join(err, x)
+ default:
+ err = errors.Join(err, fmt.Errorf("%v", x))
+ }
+ }()
+
+ p.tok = p.Scan()
+ ast = p.Config()
+ return ast, p.sc.Err()
+}
+
+func (p *Parser) err0(pos token.Position, s string, args ...any) {
+ p.sc.AddErr(pos, s, args...)
+ if p.errBudget--; p.errBudget == 0 {
+ p.sc.AddErr(pos, "too many errors")
+ }
+}
+
+func (p *Parser) err(pos token.Position, s string, args ...any) {
+ p.err0(pos, s, args...)
+ p.shift()
+ if p.eof {
+ panic(p.sc.Err())
+ }
+}
+
diff --git a/loader.go b/loader.go
new file mode 100644
index 0000000..d9bc9e4
--- /dev/null
+++ b/loader.go
@@ -0,0 +1,36 @@
+package conf
+
+import (
+ "fmt"
+ "os"
+
+ "go.neonxp.ru/conf/internal/ast"
+ "go.neonxp.ru/conf/internal/parser"
+ "go.neonxp.ru/conf/model"
+)
+
+func LoadFile(filename string) (model.Doc, error) {
+ content, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, fmt.Errorf("failed load file: %w", err)
+ }
+
+ return Load(filename, content)
+}
+
+func Load(name string, input []byte) (model.Doc, error) {
+ p := &parser.Parser{}
+ astSlice, err := p.Parse(name, input)
+ if err != nil {
+ return nil, fmt.Errorf("failed parse conf content: %w", err)
+ }
+
+ astTree := ast.Parse(p, astSlice)
+
+ doc, err := ast.ToDoc(astTree[0])
+ if err != nil {
+ return nil, fmt.Errorf("failed build Doc: %w", err)
+ }
+
+ return doc, nil
+}
diff --git a/model/doc.go b/model/doc.go
new file mode 100644
index 0000000..76f03e4
--- /dev/null
+++ b/model/doc.go
@@ -0,0 +1,18 @@
+package model
+
+type Doc []any
+
+type Assignment struct {
+ Key string
+ Value []Value
+}
+
+type Command struct {
+ Name string
+ Arguments []Value
+ Body Doc
+}
+
+type Value any
+
+type Word string