diff options
| -rw-r--r-- | config.ebnf | 28 | ||||
| -rw-r--r-- | doc.go | 2 | ||||
| -rw-r--r-- | example/file.conf | 33 | ||||
| -rw-r--r-- | example/main.go | 16 | ||||
| -rw-r--r-- | gen.go | 3 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | internal/ast/processor.go | 121 | ||||
| -rw-r--r-- | internal/ast/tree.go | 43 | ||||
| -rw-r--r-- | internal/parser/parser.go | 992 | ||||
| -rw-r--r-- | loader.go | 36 | ||||
| -rw-r--r-- | model/doc.go | 18 |
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|#.*` . @@ -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) +} @@ -0,0 +1,3 @@ +package conf + +//go:generate egg -o ./internal/parser/parser.go -package parser -start Config config.ebnf @@ -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 @@ -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 |
